├── java └── .keep ├── funppy ├── __init__.py ├── examples │ └── debugtalk.py ├── plugin.py ├── debugtalk_pb2.py └── debugtalk_pb2_grpc.py ├── myexec ├── cmd_windows_test.go ├── cmd_uixt_test.go ├── cmd_uixt.go ├── cmd_windows.go └── cmd.go ├── .gitignore ├── proto ├── debugtalk.proto └── README.md ├── pyproject.toml ├── fungo ├── examples │ ├── hashicorp.go │ └── debugtalk.go ├── init.go ├── plugin.go ├── rpc.go ├── utils.go ├── grpc.go ├── protoGen │ ├── debugtalk_grpc.pb.go │ └── debugtalk.pb.go └── utils_test.go ├── docs ├── go-rpc-plugin.md ├── CHANGELOG.md ├── python-grpc-plugin.md ├── go-plugin.md ├── go-grpc-plugin.md └── logs │ ├── hashicorp_rpc_go.log │ └── hashicorp_grpc_go.log ├── go_plugin_test.go ├── go.mod ├── .github └── workflows │ └── unittest.yml ├── go_plugin.go ├── init.go ├── hashicorp_plugin_test.go ├── README.md ├── hashicorp_plugin.go ├── go.sum ├── LICENSE └── poetry.lock /java/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /funppy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = 'v0.5.2' 2 | 3 | from funppy.plugin import register, serve 4 | 5 | __all__ = ["register", "serve"] 6 | -------------------------------------------------------------------------------- /myexec/cmd_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package myexec 4 | 5 | import "testing" 6 | 7 | func TestRunShellWindows(t *testing.T) { 8 | exitCode, err := RunShell("echo hello world") 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | if exitCode != 0 { 13 | t.Fatalf("expected exit code 0, got %d", exitCode) 14 | } 15 | 16 | exitCode, err = RunShell("dir /b /s; exit /b 3") 17 | if err == nil { 18 | t.Fatal(err) 19 | } 20 | t.Log(exitCode) 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # system or IDE generated files 15 | __debug_bin 16 | .vscode/ 17 | .idea/ 18 | .DS_Store 19 | *.bak 20 | 21 | site/ 22 | output/ 23 | 24 | # built plugins 25 | *.bin 26 | *.so 27 | 28 | # python files 29 | .venv 30 | __pycache__ 31 | .pyc 32 | dist 33 | -------------------------------------------------------------------------------- /proto/debugtalk.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package proto; 3 | 4 | option go_package = "go/protoGen"; 5 | 6 | message Empty {} 7 | 8 | message GetNamesResponse { 9 | repeated string names = 1; 10 | } 11 | 12 | message CallRequest { 13 | string name = 1; 14 | bytes args = 2; // []interface{} 15 | } 16 | 17 | message CallResponse { 18 | bytes value = 1; // interface{} 19 | } 20 | 21 | service DebugTalk { 22 | rpc GetNames(Empty) returns (GetNamesResponse); 23 | rpc Call(CallRequest) returns (CallResponse); 24 | } 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "funppy" 3 | version = "v0.5.2" 4 | description = "Python plugin over gRPC for funplugin" 5 | license = "Apache-2.0" 6 | authors = ["debugtalk "] 7 | readme = "docs/python-grpc-plugin.md" 8 | repository = "https://github.com/httprunner/funplugin" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.6" 12 | grpcio = "^1.44.0" 13 | grpcio-tools = "^1.44.0" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^5.2" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /fungo/examples/hashicorp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/httprunner/funplugin/fungo" 5 | ) 6 | 7 | // register functions and build to plugin binary 8 | func main() { 9 | fungo.Register("sum_ints", SumInts) 10 | fungo.Register("sum_two_int", SumTwoInt) 11 | fungo.Register("sum", Sum) 12 | fungo.Register("sum_two_string", SumTwoString) 13 | fungo.Register("sum_strings", SumStrings) 14 | fungo.Register("concatenate", Concatenate) 15 | fungo.Register("setup_hook_example", SetupHookExample) 16 | fungo.Register("teardown_hook_example", TeardownHookExample) 17 | 18 | // if you want to run golang plugin over net/rpc, comment out the following line 19 | // os.Setenv("HRP_PLUGIN_TYPE", "rpc") 20 | fungo.Serve() 21 | } 22 | -------------------------------------------------------------------------------- /myexec/cmd_uixt_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux 2 | 3 | package myexec 4 | 5 | import "testing" 6 | 7 | func TestRunShellUnix(t *testing.T) { 8 | testData := []struct { 9 | shell string 10 | expectExitCode int 11 | }{ 12 | {"echo hello world", 0}, 13 | {"A=123; echo $A", 0}, 14 | {"A=123 && echo $A", 0}, 15 | {"export A=123 && echo $A", 0}, 16 | {"for i in {1..3}; do echo $i; sleep 1; done", 0}, 17 | 18 | {"ls -l; exit 3", 3}, 19 | } 20 | 21 | for _, td := range testData { 22 | exitCode, err := RunShell(td.shell) 23 | if exitCode != td.expectExitCode { 24 | t.Fatalf("expected exit code 0, got %d", exitCode) 25 | } 26 | if td.expectExitCode == 0 && err != nil || 27 | td.expectExitCode != 0 && err == nil { 28 | t.Fatal(err) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/go-rpc-plugin.md: -------------------------------------------------------------------------------- 1 | # Golang plugin over net/rpc 2 | 3 | Using `golang plugin over net/rpc` is basically the same as [golang plugin over gRPC]. 4 | 5 | The only difference is that if you want to run the plugin in `net/rpc` mode, you need to set an environment variable `HRP_PLUGIN_TYPE=rpc`. 6 | 7 | Set environment variable in shell: 8 | 9 | ```bash 10 | $ export HRP_PLUGIN_TYPE=rpc 11 | ``` 12 | 13 | Or in your golang code: 14 | 15 | ```go 16 | os.Setenv("HRP_PLUGIN_TYPE", "rpc") 17 | ``` 18 | 19 | The complete log example can be found in the file [hashicorp_rpc_go.log]. 20 | 21 | [golang plugin over gRPC]: go-grpc-plugin.md 22 | [examples/plugin/]: ../examples/plugin/ 23 | [examples/plugin/debugtalk.go]: ../examples/plugin/debugtalk.go 24 | [hashicorp_rpc_go.log]: logs/hashicorp_rpc_go.log 25 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## v0.5.5 (2024-08-21) 4 | 5 | - feat: add heartbeat to keep the plugin alive 6 | - fix: set wrong env path in windows 7 | 8 | ## v0.5.4 (2024-01-18) 9 | 10 | - feat: add RunShell function to execute shell string commands and capture synchronous output 11 | - change: replace RunCommand with synchronous output 12 | - fix: check python package in windows 13 | - fix: prevent port conflicts when generating random ports 14 | 15 | ## v0.5.3 (2023-08-20) 16 | 17 | - feat: replace exec with myexec, add several exec helpers 18 | - feat: create python3 venv with funppy if python3 not specified 19 | - fix: create log file directory if not exists in InitLogger 20 | 21 | ## v0.5.2 (2023-08-10) 22 | 23 | - feat: add Init option `WithDebugLogger(debug bool)` to configure whether to print debug level logs in plugin process 24 | - feat: add Init option `WithLogFile(logFile string)` to specify log file path 25 | - feat: add Init option `WithDisableTime(disable bool)` to configure whether disable log time 26 | - refactor: merge shared utils `CallFunc` to fungo package 27 | - refactor: replace zerolog with hclog 28 | - refactor: optimize log printing for plugin 29 | - fix: ensure using grpc for hashicorp python plugin 30 | -------------------------------------------------------------------------------- /go_plugin_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd || darwin 2 | // +build linux freebsd darwin 3 | 4 | // go plugin doesn't support windows 5 | 6 | package funplugin 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "testing" 12 | 13 | "github.com/httprunner/funplugin/myexec" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func buildGoPlugin() { 18 | fmt.Println("[setup] build go plugin") 19 | // flag -race is necessary in order to be consistent with go test 20 | err := myexec.RunCommand("go", "build", "-buildmode=plugin", "-race", 21 | "-o=debugtalk.so", "fungo/examples/debugtalk.go") 22 | if err != nil { 23 | panic(err) 24 | } 25 | } 26 | 27 | func removeGoPlugin() { 28 | fmt.Println("[teardown] remove go plugin") 29 | os.Remove("debugtalk.so") 30 | } 31 | 32 | func TestCallPluginFunction(t *testing.T) { 33 | buildGoPlugin() 34 | defer removeGoPlugin() 35 | 36 | plugin, err := Init("debugtalk.so", WithDebugLogger(true)) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | if !assert.True(t, plugin.Has("Concatenate")) { 42 | t.Fail() 43 | } 44 | 45 | // call function with arguments 46 | result, err := plugin.Call("Concatenate", "1", 2, "3.14") 47 | if !assert.NoError(t, err) { 48 | t.Fail() 49 | } 50 | if !assert.Equal(t, "123.14", result) { 51 | t.Fail() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /fungo/examples/debugtalk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func init() { 9 | log.Println("plugin init function called") 10 | } 11 | 12 | func SumTwoInt(a, b int) int { 13 | return a + b 14 | } 15 | 16 | func SumInts(args ...int) int { 17 | var sum int 18 | for _, arg := range args { 19 | sum += arg 20 | } 21 | return sum 22 | } 23 | 24 | func Sum(args ...interface{}) (interface{}, error) { 25 | var sum float64 26 | for _, arg := range args { 27 | switch v := arg.(type) { 28 | case int: 29 | sum += float64(v) 30 | case float64: 31 | sum += v 32 | default: 33 | return nil, fmt.Errorf("unexpected type: %T", arg) 34 | } 35 | } 36 | return sum, nil 37 | } 38 | 39 | func SumTwoString(a, b string) string { 40 | return a + b 41 | } 42 | 43 | func SumStrings(s ...string) string { 44 | var sum string 45 | for _, arg := range s { 46 | sum += arg 47 | } 48 | return sum 49 | } 50 | 51 | func Concatenate(args ...interface{}) (interface{}, error) { 52 | var result string 53 | for _, arg := range args { 54 | result += fmt.Sprintf("%v", arg) 55 | } 56 | return result, nil 57 | } 58 | 59 | func SetupHookExample(args string) string { 60 | return fmt.Sprintf("step name: %v, setup...", args) 61 | } 62 | 63 | func TeardownHookExample(args string) string { 64 | return fmt.Sprintf("step name: %v, teardown...", args) 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/httprunner/funplugin 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hashicorp/go-hclog v1.5.0 7 | github.com/hashicorp/go-plugin v1.4.10 8 | github.com/json-iterator/go v1.1.12 9 | github.com/pkg/errors v0.9.1 10 | github.com/stretchr/testify v1.8.4 11 | google.golang.org/grpc v1.57.0 12 | google.golang.org/protobuf v1.31.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/fatih/color v1.15.0 // indirect 18 | github.com/golang/protobuf v1.5.3 // indirect 19 | github.com/hashicorp/yamux v0.1.1 // indirect 20 | github.com/kr/pretty v0.3.0 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.19 // indirect 23 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/oklog/run v1.1.0 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/rogpeppe/go-internal v1.9.0 // indirect 29 | golang.org/x/net v0.12.0 // indirect 30 | golang.org/x/sys v0.10.0 // indirect 31 | golang.org/x/text v0.11.0 // indirect 32 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e // indirect 33 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /funppy/examples/debugtalk.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | 5 | def sum(*args): 6 | result = 0 7 | for arg in args: 8 | result += arg 9 | return result 10 | 11 | def sum_ints(*args: List[int]) -> int: 12 | result = 0 13 | for arg in args: 14 | result += arg 15 | return result 16 | 17 | def sum_two_int(a: int, b: int) -> int: 18 | return a + b 19 | 20 | def sum_two_string(a: str, b: str) -> str: 21 | return a + b 22 | 23 | def sum_strings(*args: List[str]) -> str: 24 | result = "" 25 | for arg in args: 26 | result += arg 27 | return result 28 | 29 | def concatenate(*args: List[str]) -> str: 30 | result = "" 31 | for arg in args: 32 | result += str(arg) 33 | return result 34 | 35 | def setup_hook_example(name): 36 | logging.warn("setup_hook_example") 37 | return f"setup_hook_example: {name}" 38 | 39 | def teardown_hook_example(name): 40 | logging.warn("teardown_hook_example") 41 | return f"teardown_hook_example: {name}" 42 | 43 | 44 | if __name__ == '__main__': 45 | import funppy 46 | funppy.register("sum", sum) 47 | funppy.register("sum_ints", sum_ints) 48 | funppy.register("concatenate", concatenate) 49 | funppy.register("sum_two_int", sum_two_int) 50 | funppy.register("sum_two_string", sum_two_string) 51 | funppy.register("sum_strings", sum_strings) 52 | funppy.register("setup_hook_example", setup_hook_example) 53 | funppy.register("teardown_hook_example", teardown_hook_example) 54 | funppy.serve() 55 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Run unittests for go plugin 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [synchronize] 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | go-version: 14 | - 1.18.x 15 | - 1.19.x 16 | - 1.20.x 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | - name: Install Python plugin dependencies for macos 25 | if: matrix.os == 'macos-latest' 26 | run: | 27 | python3 -m venv .venv 28 | source .venv/bin/activate 29 | python3 -m pip install funppy 30 | - name: Install Python plugin dependencies for linux/windows 31 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest' 32 | run: python3 -m pip install funppy 33 | - name: Checkout code 34 | uses: actions/checkout@v2 35 | - name: Run coverage 36 | run: go test -coverprofile="cover.out" -covermode=atomic -race ./... 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v4 39 | with: 40 | name: hrp plugin # User defined upload name. Visible in Codecov UI 41 | token: ${{ secrets.CODECOV_TOKEN }} # Repository upload token 42 | file: ./cover.out # Path to coverage file to upload 43 | flags: unittests # Flag upload to group coverage metrics 44 | fail_ci_if_error: true # Specify whether or not CI build should fail if Codecov runs into an error during upload 45 | verbose: true 46 | -------------------------------------------------------------------------------- /docs/python-grpc-plugin.md: -------------------------------------------------------------------------------- 1 | # Python plugin over gRPC 2 | 3 | ## install SDK 4 | 5 | Before you develop your python plugin, you need to install an dependency as SDK. 6 | 7 | ```bash 8 | $ python3 -m pip install funppy 9 | ``` 10 | 11 | ## create plugin functions 12 | 13 | Then you can write your plugin functions in python. The functions can be very flexible, only the following restrictions should be complied with. 14 | 15 | - function should return at most one value and one error. 16 | - `funppy.register()` must be called to register plugin functions and `funppy.serve()` must be called to start a plugin server process. 17 | 18 | Here is some plugin functions as example. 19 | 20 | ```python 21 | import logging 22 | from typing import List 23 | 24 | import funppy 25 | 26 | 27 | def sum_two_int(a: int, b: int) -> int: 28 | return a + b 29 | 30 | def sum_ints(*args: List[int]) -> int: 31 | result = 0 32 | for arg in args: 33 | result += arg 34 | return result 35 | 36 | def Sum(*args): 37 | result = 0 38 | for arg in args: 39 | result += arg 40 | return result 41 | 42 | 43 | if __name__ == '__main__': 44 | funppy.register("sum_two_int", sum_two_int) 45 | funppy.register("sum_ints", sum_ints) 46 | funppy.register("sum", Sum) 47 | funppy.serve() 48 | ``` 49 | 50 | You can get more examples at [funppy/examples/]. 51 | 52 | ## build plugin 53 | 54 | Python plugins do not need to be complied, just make sure its file suffix is `.py` by convention and should not be changed. 55 | 56 | ## use plugin functions 57 | 58 | Finally, you can use `Init` to initialize plugin via the `xxx.py` path, and you can call the plugin API to handle plugin functionality. 59 | 60 | 61 | [funppy/examples/]: ../funppy/examples/ 62 | -------------------------------------------------------------------------------- /docs/go-plugin.md: -------------------------------------------------------------------------------- 1 | 2 | # go plugin 3 | 4 | The golang official plugin is only supported on `Linux`, `FreeBSD`, and `macOS`. And this solution also has many drawbacks. 5 | 6 | ## create plugin functions 7 | 8 | Firstly, you need to define your plugin functions. The functions can be very flexible, only the following restrictions should be complied with. 9 | 10 | - plugin package name must be `main`. 11 | - function names must be capitalized. 12 | - function should return at most one value and one error. 13 | 14 | Here is some plugin functions as example. 15 | 16 | ```go 17 | package main 18 | 19 | func SumTwoInt(a, b int) int { 20 | return a + b 21 | } 22 | 23 | func SumInts(args ...int) int { 24 | var sum int 25 | for _, arg := range args { 26 | sum += arg 27 | } 28 | return sum 29 | } 30 | 31 | func Sum(args ...interface{}) (interface{}, error) { 32 | var sum float64 33 | for _, arg := range args { 34 | switch v := arg.(type) { 35 | case int: 36 | sum += float64(v) 37 | case float64: 38 | sum += v 39 | default: 40 | return nil, fmt.Errorf("unexpected type: %T", arg) 41 | } 42 | } 43 | return sum, nil 44 | } 45 | ``` 46 | 47 | You can get more examples at [fungo/examples/debugtalk.go] 48 | 49 | ## build plugin 50 | 51 | Then you can build your go plugin with `-buildmode=plugin` flag to binary file `xxx.so`. The file suffix of `.so` is by convention and should not be changed. 52 | 53 | ```bash 54 | $ go build -buildmode=plugin -o=fungo/examples/xxx.so fungo/examples/debugtalk.go 55 | ``` 56 | 57 | ## use plugin functions 58 | 59 | Finally, you can use `Init` to initialize plugin via the `xxx.so` path, and you can call the plugin API to handle plugin functionality. 60 | 61 | Notice: you should use the original function name. 62 | 63 | [fungo/examples/debugtalk.go]: ../fungo/examples/debugtalk.go 64 | -------------------------------------------------------------------------------- /myexec/cmd_uixt.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux 2 | 3 | package myexec 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "syscall" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func getPython3Executable(venvDir string) string { 16 | return filepath.Join(venvDir, "bin", "python3") 17 | } 18 | 19 | func ensurePython3Venv(venv string, packages ...string) (python3 string, err error) { 20 | python3 = getPython3Executable(venv) 21 | 22 | logger.Info("ensure python3 venv", 23 | "python3", python3, 24 | "packages", packages) 25 | 26 | // check if python3 venv is available 27 | if !isPython3(python3) { 28 | // python3 venv not available, create one 29 | // check if system python3 is available 30 | if err := RunCommand("python3", "--version"); err != nil { 31 | return "", errors.Wrap(err, "python3 not found") 32 | } 33 | 34 | // check if .venv exists 35 | if _, err := os.Stat(venv); err == nil { 36 | // .venv exists, remove first 37 | if err := RunCommand("rm", "-rf", venv); err != nil { 38 | return "", errors.Wrap(err, "remove existed venv failed") 39 | } 40 | } 41 | 42 | // create python3 .venv 43 | if err := RunCommand("python3", "-m", "venv", venv); err != nil { 44 | return "", errors.Wrap(err, "create python3 venv failed") 45 | } 46 | } 47 | 48 | // install default python packages 49 | for _, pkg := range packages { 50 | err := InstallPythonPackage(python3, pkg) 51 | if err != nil { 52 | return "", errors.Wrap(err, fmt.Sprintf("pip install %s failed", pkg)) 53 | } 54 | } 55 | 56 | return python3, nil 57 | } 58 | 59 | func Command(name string, arg ...string) *exec.Cmd { 60 | cmd := exec.Command(name, arg...) 61 | cmd.SysProcAttr = &syscall.SysProcAttr{ 62 | Setpgid: true, 63 | } 64 | return cmd 65 | } 66 | 67 | func KillProcessesByGpid(cmd *exec.Cmd) error { 68 | return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 69 | } 70 | 71 | func initShellExec(shellString string) *exec.Cmd { 72 | // bash -c shellString 73 | return exec.Command("bash", "-c", shellString) 74 | } 75 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | # Updating the Protocol 2 | 3 | If you update the protocol buffers file, you can regenerate the file using the following command from the project root directory. You do not need to run this if you're just using the plugin. 4 | 5 | ## For Go 6 | 7 | ### Install dependencies 8 | 9 | ref: https://www.grpc.io/docs/languages/go/quickstart/ 10 | 11 | Install the protocol compiler plugins for Go using the following commands: 12 | 13 | ```bash 14 | $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 15 | $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 16 | ``` 17 | 18 | Update your PATH so that the protoc compiler can find the plugins: 19 | 20 | ```bash 21 | $ export PATH="$PATH:$(go env GOPATH)/bin" 22 | ``` 23 | 24 | ### Generate gRPC code 25 | 26 | ```bash 27 | $ protoc --go_out=. --go-grpc_out=. proto/debugtalk.proto 28 | ``` 29 | 30 | This will generate two go files in `go/protoGen` folder: 31 | 32 | - debugtalk.pb.go 33 | - debugtalk_grpc.pb.go 34 | 35 | ## For Python 36 | 37 | ### Install dependencies 38 | 39 | ref: https://www.grpc.io/docs/languages/python/quickstart/ 40 | 41 | Install gRPC: 42 | 43 | ```bash 44 | $ pip3 install grpcio 45 | ``` 46 | 47 | Install gRPC tools: 48 | 49 | ```bash 50 | $ pip3 install grpcio-tools 51 | ``` 52 | 53 | Or you can just install all dependencies with `poetry`. 54 | 55 | ```bash 56 | $ poetry install 57 | ``` 58 | 59 | ### Generate gRPC code 60 | 61 | ```bash 62 | $ python3 -m grpc_tools.protoc -I=proto --python_out=funppy/ --grpc_python_out=funppy/ proto/debugtalk.proto 63 | ``` 64 | 65 | This will generate two python files in `python` folder: 66 | 67 | - debugtalk_pb2.py 68 | - debugtalk_pb2_grpc.py 69 | 70 | We need to modify `debugtalk_pb2_grpc.py` from `import debugtalk_pb2 as debugtalk__pb2` to `from funppy import debugtalk_pb2 as debugtalk__pb2`. 71 | 72 | Or we can generate target files like this: 73 | 74 | ```bash 75 | $ cp proto/debugtalk.proto funppy/debugtalk.proto 76 | $ python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. funppy/debugtalk.proto 77 | ``` 78 | -------------------------------------------------------------------------------- /go_plugin.go: -------------------------------------------------------------------------------- 1 | package funplugin 2 | 3 | import ( 4 | "fmt" 5 | "plugin" 6 | "reflect" 7 | "runtime" 8 | 9 | "github.com/httprunner/funplugin/fungo" 10 | ) 11 | 12 | // goPlugin implements golang official plugin 13 | type goPlugin struct { 14 | *plugin.Plugin 15 | path string // plugin file path 16 | cachedFunctions map[string]reflect.Value // cache loaded functions to improve performance 17 | } 18 | 19 | func newGoPlugin(path string) (*goPlugin, error) { 20 | if runtime.GOOS == "windows" { 21 | logger.Warn("go plugin does not support windows") 22 | return nil, fmt.Errorf("go plugin does not support windows") 23 | } 24 | 25 | // logger 26 | logger = logger.ResetNamed("go-plugin") 27 | 28 | plg, err := plugin.Open(path) 29 | if err != nil { 30 | logger.Error("load go plugin failed", "path", path, "error", err) 31 | return nil, err 32 | } 33 | 34 | logger.Info("load go plugin success", "path", path) 35 | p := &goPlugin{ 36 | Plugin: plg, 37 | path: path, 38 | cachedFunctions: make(map[string]reflect.Value), 39 | } 40 | return p, nil 41 | } 42 | 43 | func (p *goPlugin) Type() string { 44 | return "go-plugin" 45 | } 46 | 47 | func (p *goPlugin) Path() string { 48 | return p.path 49 | } 50 | 51 | func (p *goPlugin) Has(funcName string) bool { 52 | logger.Debug("check if plugin has function", "funcName", funcName) 53 | fn, ok := p.cachedFunctions[funcName] 54 | if ok { 55 | return fn.IsValid() 56 | } 57 | 58 | sym, err := p.Plugin.Lookup(funcName) 59 | if err != nil { 60 | p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid 61 | return false 62 | } 63 | fn = reflect.ValueOf(sym) 64 | 65 | // check function type 66 | if fn.Kind() != reflect.Func { 67 | p.cachedFunctions[funcName] = reflect.Value{} // mark as invalid 68 | return false 69 | } 70 | 71 | p.cachedFunctions[funcName] = fn 72 | return true 73 | } 74 | 75 | func (p *goPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { 76 | if !p.Has(funcName) { 77 | return nil, fmt.Errorf("function %s not found", funcName) 78 | } 79 | fn := p.cachedFunctions[funcName] 80 | return fungo.CallFunc(fn, args...) 81 | } 82 | 83 | func (p *goPlugin) Quit() error { 84 | // no need to quit for go plugin 85 | return nil 86 | } 87 | 88 | func (p *goPlugin) StartHeartbeat() { 89 | 90 | } 91 | -------------------------------------------------------------------------------- /docs/go-grpc-plugin.md: -------------------------------------------------------------------------------- 1 | # Golang plugin over gRPC 2 | 3 | It is recommended to use `golang plugin over gRPC` in most cases. This should be the most stable solution with higher performance. 4 | 5 | ## install SDK 6 | 7 | Before you develop your golang plugin, you need to install an dependency as SDK. 8 | 9 | ```bash 10 | $ go get -u github.com/httprunner/funplugin 11 | ``` 12 | 13 | ## create plugin functions 14 | 15 | Then you can write your plugin functions in golang. The functions can be very flexible, only the following restrictions should be complied with. 16 | 17 | - package name should be `main`. 18 | - function should return at most one value and one error. 19 | - in `main()` function, `Register()` must be called to register plugin functions and `Serve()` must be called to start a plugin server process. 20 | 21 | Here is some plugin functions as example. 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "fmt" 28 | 29 | "github.com/httprunner/funplugin/fungo" 30 | ) 31 | 32 | func SumTwoInt(a, b int) int { 33 | return a + b 34 | } 35 | 36 | func SumInts(args ...int) int { 37 | var sum int 38 | for _, arg := range args { 39 | sum += arg 40 | } 41 | return sum 42 | } 43 | 44 | func Sum(args ...interface{}) (interface{}, error) { 45 | var sum float64 46 | for _, arg := range args { 47 | switch v := arg.(type) { 48 | case int: 49 | sum += float64(v) 50 | case float64: 51 | sum += v 52 | default: 53 | return nil, fmt.Errorf("unexpected type: %T", arg) 54 | } 55 | } 56 | return sum, nil 57 | } 58 | 59 | func main() { 60 | fungo.Register("sum_ints", SumInts) 61 | fungo.Register("sum_two_int", SumTwoInt) 62 | fungo.Register("sum", Sum) 63 | fungo.Serve() 64 | } 65 | ``` 66 | 67 | You can get more examples at [fungo/examples/]. 68 | 69 | ## build plugin 70 | 71 | Once the plugin functions are ready, you can build them into the binary file `xxx.bin`. The file suffix of `.bin` is by convention and should not be changed. 72 | 73 | ```bash 74 | $ go build -o fungo/examples/xxx.bin fungo/examples/hashicorp.go fungo/examples/debugtalk.go 75 | ``` 76 | 77 | ## use plugin functions 78 | 79 | Finally, you can use `Init` to initialize plugin via the `xxx.bin` path, and you can call the plugin API to handle plugin functionality. 80 | 81 | The complete log example can be found in the file [hashicorp_grpc_go.log]. 82 | 83 | 84 | [fungo/examples/]: ../fungo/examples/ 85 | [hashicorp_grpc_go.log]: logs/hashicorp_grpc_go.log 86 | -------------------------------------------------------------------------------- /fungo/init.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | hclog "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | ) 11 | 12 | const Version = "v0.5.4" 13 | 14 | var ( 15 | logger = Logger 16 | ) 17 | 18 | var Logger = hclog.New(&hclog.LoggerOptions{ 19 | Name: "fungo", 20 | Output: hclog.DefaultOutput, 21 | DisableTime: true, 22 | Level: hclog.Debug, 23 | Color: hclog.AutoColor, 24 | }) 25 | 26 | var file *os.File 27 | 28 | func InitLogger(logLevel hclog.Level, logFile string, disableTime bool) hclog.Logger { 29 | output := hclog.DefaultOutput 30 | if logFile != "" { 31 | err := os.MkdirAll(filepath.Dir(logFile), os.ModePerm) 32 | if err != nil { 33 | logger.Error("create log file directory failed", 34 | "error", err, "logFile", logFile) 35 | os.Exit(1) 36 | } 37 | 38 | file, err = os.OpenFile(logFile, os.O_CREATE|os.O_RDWR, 0666) 39 | if err != nil { 40 | logger.Error("open log file failed", "error", err) 41 | os.Exit(1) 42 | } 43 | output = io.MultiWriter(hclog.DefaultOutput, file) 44 | } 45 | 46 | logger = hclog.New(&hclog.LoggerOptions{ 47 | Name: "fungo", 48 | Output: output, 49 | DisableTime: disableTime, 50 | Level: logLevel, 51 | Color: hclog.AutoColor, 52 | }) 53 | logger.Info("set plugin log level", 54 | "level", logLevel.String(), "logFile", logFile) 55 | return logger 56 | } 57 | 58 | func CloseLogFile() error { 59 | if file != nil { 60 | logger.Info("close log file") 61 | return file.Close() 62 | } 63 | return nil 64 | } 65 | 66 | // PluginTypeEnvName is used to specify hashicorp go plugin type, rpc/grpc 67 | const PluginTypeEnvName = "HRP_PLUGIN_TYPE" 68 | 69 | // HandshakeConfig is used to just do a basic handshake between 70 | // a plugin and host. If the handshake fails, a user friendly error is shown. 71 | // This prevents users from executing bad plugins or executing a plugin 72 | // directory. It is a UX feature, not a security feature. 73 | var HandshakeConfig = plugin.HandshakeConfig{ 74 | ProtocolVersion: 1, 75 | MagicCookieKey: "HttpRunnerPlus", 76 | MagicCookieValue: "debugtalk", 77 | } 78 | 79 | // IFuncCaller is the interface that we're exposing as a plugin. 80 | type IFuncCaller interface { 81 | GetNames() ([]string, error) // get all plugin function names list 82 | Call(funcName string, args ...interface{}) (interface{}, error) // call plugin function 83 | } 84 | -------------------------------------------------------------------------------- /funppy/plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import random 4 | import sys 5 | import time 6 | import socket 7 | from concurrent import futures 8 | from typing import Callable 9 | 10 | import grpc 11 | 12 | from funppy import debugtalk_pb2, debugtalk_pb2_grpc 13 | 14 | __all__ = ["register", "serve"] 15 | 16 | functions = {} 17 | 18 | 19 | def register(func_name: str, func: Callable): 20 | logging.info(f"register function: {func_name}") 21 | functions[func_name] = func 22 | 23 | 24 | class DebugTalkServicer(debugtalk_pb2_grpc.DebugTalkServicer): 25 | """Implementation of DebugTalk service.""" 26 | 27 | def GetNames(self, request: debugtalk_pb2.Empty, context: grpc.ServicerContext): 28 | names = list(functions.keys()) 29 | response = debugtalk_pb2.GetNamesResponse(names=names) 30 | return response 31 | 32 | def Call(self, request: debugtalk_pb2.CallRequest, context: grpc.ServicerContext): 33 | if request.name not in functions: 34 | raise Exception(f"Function {request.name} not registered!") 35 | 36 | fn = functions[request.name] 37 | args = json.loads(request.args) 38 | value = fn(*args) 39 | 40 | if isinstance(value, (int, float)): 41 | v = str(value).encode("utf-8") 42 | elif isinstance(value, (str, dict, list)): 43 | v = json.dumps(value).encode("utf-8") 44 | else: 45 | raise Exception(f"Function return type {type(value)} not supported!") 46 | 47 | response = debugtalk_pb2.CallResponse(value=v) 48 | return response 49 | 50 | 51 | def get_available_port() -> int: 52 | while True: 53 | random_port = random.randrange(20000, 60000) 54 | 55 | try: 56 | # Create a socket object and attempt to bind it to the specified port 57 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 58 | s.bind(("127.0.0.1", random_port)) 59 | 60 | # The port is available 61 | return random_port 62 | 63 | except OSError: 64 | # The port is already in use, regenerate 65 | continue 66 | 67 | 68 | def serve(): 69 | # Start the server. 70 | 71 | # Generate a random port 72 | random_port = get_available_port() 73 | 74 | # Create the gRPC server and continue with the rest of your code 75 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 76 | debugtalk_pb2_grpc.add_DebugTalkServicer_to_server(DebugTalkServicer(), server) 77 | 78 | server.add_insecure_port(f"127.0.0.1:{random_port}") 79 | server.start() 80 | 81 | # Output information 82 | print(f"1|1|tcp|127.0.0.1:{random_port}|grpc") 83 | sys.stdout.flush() 84 | 85 | try: 86 | while True: 87 | time.sleep(60 * 60 * 24) 88 | except KeyboardInterrupt: 89 | server.stop(0) 90 | 91 | 92 | if __name__ == "__main__": 93 | serve() 94 | -------------------------------------------------------------------------------- /fungo/plugin.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | 8 | hclog "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | ) 11 | 12 | // functionsMap stores plugin functions 13 | type functionsMap map[string]reflect.Value 14 | 15 | // functionPlugin implements the FuncCaller interface 16 | type functionPlugin struct { 17 | logger hclog.Logger 18 | functions functionsMap 19 | } 20 | 21 | func (p *functionPlugin) GetNames() ([]string, error) { 22 | var names []string 23 | for name := range p.functions { 24 | names = append(names, name) 25 | } 26 | p.logger.Debug("get registered plugin functions", "names", names) 27 | return names, nil 28 | } 29 | 30 | func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { 31 | // notice: this is the actual place where plugin function is called 32 | p.logger.Debug("plugin function execution", "funcName", funcName, "args", args) 33 | 34 | fn, ok := p.functions[funcName] 35 | if !ok { 36 | return nil, fmt.Errorf("function %s not found", funcName) 37 | } 38 | 39 | return CallFunc(fn, args...) 40 | } 41 | 42 | var functions = make(functionsMap) 43 | 44 | // Register registers a plugin function. 45 | // Every plugin function must be registered before Serve() is called. 46 | func Register(funcName string, fn interface{}) { 47 | if _, ok := functions[funcName]; ok { 48 | return 49 | } 50 | logger.Info("register plugin function", "funcName", funcName) 51 | functions[funcName] = reflect.ValueOf(fn) 52 | // automatic registration with common name 53 | functions[ConvertCommonName(funcName)] = functions[funcName] 54 | } 55 | 56 | // serveRPC starts a plugin server process in RPC mode. 57 | func serveRPC() { 58 | rpcPluginName := "rpc" 59 | logger.Info("start plugin server in RPC mode") 60 | funcPlugin := &functionPlugin{ 61 | logger: logger.Named("func_exec"), 62 | functions: functions, 63 | } 64 | var pluginMap = map[string]plugin.Plugin{ 65 | rpcPluginName: &RPCPlugin{Impl: funcPlugin}, 66 | } 67 | // start RPC server 68 | plugin.Serve(&plugin.ServeConfig{ 69 | HandshakeConfig: HandshakeConfig, 70 | Plugins: pluginMap, 71 | }) 72 | } 73 | 74 | // serveGRPC starts a plugin server process in gRPC mode. 75 | func serveGRPC() { 76 | grpcPluginName := "grpc" 77 | logger.Info("start plugin server in gRPC mode") 78 | funcPlugin := &functionPlugin{ 79 | logger: logger.Named("func_exec"), 80 | functions: functions, 81 | } 82 | var pluginMap = map[string]plugin.Plugin{ 83 | grpcPluginName: &GRPCPlugin{Impl: funcPlugin}, 84 | } 85 | // start gRPC server 86 | plugin.Serve(&plugin.ServeConfig{ 87 | HandshakeConfig: HandshakeConfig, 88 | Plugins: pluginMap, 89 | GRPCServer: plugin.DefaultGRPCServer, 90 | }) 91 | } 92 | 93 | // default to run plugin in gRPC mode 94 | func Serve() { 95 | if os.Getenv(PluginTypeEnvName) == "rpc" { 96 | serveRPC() 97 | } else { 98 | // default 99 | serveGRPC() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /fungo/rpc.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "encoding/gob" 5 | "net/rpc" 6 | 7 | "github.com/hashicorp/go-plugin" 8 | ) 9 | 10 | func init() { 11 | gob.Register(new(funcData)) 12 | } 13 | 14 | // funcData is used to transfer between plugin and host via RPC. 15 | type funcData struct { 16 | Name string // function name 17 | Args []interface{} // function arguments 18 | } 19 | 20 | // functionRPCClient runs on the host side, it implements FuncCaller interface 21 | type functionRPCClient struct { 22 | client *rpc.Client 23 | } 24 | 25 | func (g *functionRPCClient) GetNames() ([]string, error) { 26 | logger.Debug("rpc_client GetNames() start") 27 | var resp []string 28 | err := g.client.Call("Plugin.GetNames", new(interface{}), &resp) 29 | if err != nil { 30 | logger.Error("rpc_client GetNames() failed", "error", err) 31 | return nil, err 32 | } 33 | logger.Debug("rpc_client GetNames() success") 34 | return resp, nil 35 | } 36 | 37 | // host -> plugin 38 | func (g *functionRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { 39 | logger.Info("rpc_client Call() start", "funcName", funcName, "funcArgs", funcArgs) 40 | f := funcData{ 41 | Name: funcName, 42 | Args: funcArgs, 43 | } 44 | 45 | var args interface{} = f 46 | var resp interface{} 47 | err := g.client.Call("Plugin.Call", &args, &resp) 48 | if err != nil { 49 | logger.Error("rpc_client Call() failed", 50 | "funcName", funcName, 51 | "funcArgs", funcArgs, 52 | "error", err, 53 | ) 54 | return nil, err 55 | } 56 | logger.Info("rpc_client Call() success", "result", resp) 57 | return resp, nil 58 | } 59 | 60 | // functionRPCServer runs on the plugin side, executing the user custom function. 61 | type functionRPCServer struct { 62 | Impl IFuncCaller 63 | } 64 | 65 | // plugin execution 66 | func (s *functionRPCServer) GetNames(args interface{}, resp *[]string) error { 67 | logger.Debug("rpc_server GetNames() start") 68 | var err error 69 | *resp, err = s.Impl.GetNames() 70 | if err != nil { 71 | logger.Error("rpc_server GetNames() failed", "error", err) 72 | return err 73 | } 74 | logger.Debug("rpc_server GetNames() success") 75 | return nil 76 | } 77 | 78 | // plugin execution 79 | func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error { 80 | logger.Debug("rpc_server Call() start") 81 | f := args.(*funcData) 82 | var err error 83 | *resp, err = s.Impl.Call(f.Name, f.Args...) 84 | if err != nil { 85 | logger.Error("rpc_server Call() failed", "args", args, "error", err) 86 | return err 87 | } 88 | logger.Debug("rpc_server Call() success") 89 | return nil 90 | } 91 | 92 | // RPCPlugin implements hashicorp's plugin.Plugin. 93 | type RPCPlugin struct { 94 | Impl IFuncCaller 95 | } 96 | 97 | func (p *RPCPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 98 | return &functionRPCServer{Impl: p.Impl}, nil 99 | } 100 | 101 | func (p *RPCPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 102 | return &functionRPCClient{client: c}, nil 103 | } 104 | -------------------------------------------------------------------------------- /funppy/debugtalk_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: debugtalk.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x64\x65\x62ugtalk.proto\x12\x05proto\"\x07\n\x05\x45mpty\"!\n\x10GetNamesResponse\x12\r\n\x05names\x18\x01 \x03(\t\")\n\x0b\x43\x61llRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x01(\x0c\"\x1d\n\x0c\x43\x61llResponse\x12\r\n\x05value\x18\x01 \x01(\x0c\x32o\n\tDebugTalk\x12\x31\n\x08GetNames\x12\x0c.proto.Empty\x1a\x17.proto.GetNamesResponse\x12/\n\x04\x43\x61ll\x12\x12.proto.CallRequest\x1a\x13.proto.CallResponseB\rZ\x0bgo/protoGenb\x06proto3') 18 | 19 | 20 | 21 | _EMPTY = DESCRIPTOR.message_types_by_name['Empty'] 22 | _GETNAMESRESPONSE = DESCRIPTOR.message_types_by_name['GetNamesResponse'] 23 | _CALLREQUEST = DESCRIPTOR.message_types_by_name['CallRequest'] 24 | _CALLRESPONSE = DESCRIPTOR.message_types_by_name['CallResponse'] 25 | Empty = _reflection.GeneratedProtocolMessageType('Empty', (_message.Message,), { 26 | 'DESCRIPTOR' : _EMPTY, 27 | '__module__' : 'debugtalk_pb2' 28 | # @@protoc_insertion_point(class_scope:proto.Empty) 29 | }) 30 | _sym_db.RegisterMessage(Empty) 31 | 32 | GetNamesResponse = _reflection.GeneratedProtocolMessageType('GetNamesResponse', (_message.Message,), { 33 | 'DESCRIPTOR' : _GETNAMESRESPONSE, 34 | '__module__' : 'debugtalk_pb2' 35 | # @@protoc_insertion_point(class_scope:proto.GetNamesResponse) 36 | }) 37 | _sym_db.RegisterMessage(GetNamesResponse) 38 | 39 | CallRequest = _reflection.GeneratedProtocolMessageType('CallRequest', (_message.Message,), { 40 | 'DESCRIPTOR' : _CALLREQUEST, 41 | '__module__' : 'debugtalk_pb2' 42 | # @@protoc_insertion_point(class_scope:proto.CallRequest) 43 | }) 44 | _sym_db.RegisterMessage(CallRequest) 45 | 46 | CallResponse = _reflection.GeneratedProtocolMessageType('CallResponse', (_message.Message,), { 47 | 'DESCRIPTOR' : _CALLRESPONSE, 48 | '__module__' : 'debugtalk_pb2' 49 | # @@protoc_insertion_point(class_scope:proto.CallResponse) 50 | }) 51 | _sym_db.RegisterMessage(CallResponse) 52 | 53 | _DEBUGTALK = DESCRIPTOR.services_by_name['DebugTalk'] 54 | if _descriptor._USE_C_DESCRIPTORS == False: 55 | 56 | DESCRIPTOR._options = None 57 | DESCRIPTOR._serialized_options = b'Z\013go/protoGen' 58 | _EMPTY._serialized_start=26 59 | _EMPTY._serialized_end=33 60 | _GETNAMESRESPONSE._serialized_start=35 61 | _GETNAMESRESPONSE._serialized_end=68 62 | _CALLREQUEST._serialized_start=70 63 | _CALLREQUEST._serialized_end=111 64 | _CALLRESPONSE._serialized_start=113 65 | _CALLRESPONSE._serialized_end=142 66 | _DEBUGTALK._serialized_start=144 67 | _DEBUGTALK._serialized_end=255 68 | # @@protoc_insertion_point(module_scope) 69 | -------------------------------------------------------------------------------- /myexec/cmd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package myexec 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strconv" 11 | "syscall" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func init() { 17 | // use python instead of python3 if python3 is not available 18 | if !isPython3(python3Executable) { 19 | python3Executable = "python" 20 | } 21 | } 22 | 23 | func getPython3Executable(venvDir string) string { 24 | python := filepath.Join(venvDir, "Scripts", "python3.exe") 25 | if isPython3(python) { 26 | return python 27 | } 28 | return filepath.Join(venvDir, "Scripts", "python.exe") 29 | } 30 | 31 | func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err error) { 32 | python3 = getPython3Executable(venvDir) 33 | logger.Info("ensure python3 venv", 34 | "python3", python3, 35 | "packages", packages) 36 | 37 | systemPython := "python3" 38 | 39 | // check if python3 venv is available 40 | if !isPython3(python3) { 41 | // python3 venv not available, create one 42 | // check if system python3 is available 43 | logger.Warn("python3 venv is not available, try to check system python3", 44 | "pythonPath", python3) 45 | if !isPython3(systemPython) { 46 | if !isPython3("python") { 47 | return "", errors.Wrap(err, "python3 not found") 48 | } 49 | systemPython = "python" 50 | } 51 | 52 | // check if .venv exists 53 | if _, err := os.Stat(venvDir); err == nil { 54 | // .venv exists, remove first 55 | if err := RunCommand("del", "/q", venvDir); err != nil { 56 | return "", errors.Wrap(err, "remove existed venv failed") 57 | } 58 | } 59 | 60 | // create python3 .venv 61 | // notice: --symlinks should be specified for windows 62 | // https://github.com/actions/virtual-environments/issues/2690 63 | if err := RunCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil { 64 | // fix: failed to symlink on Windows 65 | logger.Warn("failed to create python3 .venv by using --symlinks, try to use --copies") 66 | if err := RunCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil { 67 | return "", errors.Wrap(err, "create python3 venv failed") 68 | } 69 | } 70 | 71 | // fix: python3 doesn't exist in .venv on Windows 72 | if _, err := os.Stat(python3); err != nil { 73 | logger.Warn("python3 doesn't exist, try to link python") 74 | err := os.Link(filepath.Join(venvDir, "Scripts", "python.exe"), python3) 75 | if err != nil { 76 | return "", errors.Wrap(err, "python3 doesn't exist in .venv") 77 | } 78 | } 79 | } 80 | 81 | // install default python packages 82 | for _, pkg := range packages { 83 | err := InstallPythonPackage(python3, pkg) 84 | if err != nil { 85 | return "", errors.Wrap(err, fmt.Sprintf("pip install %s failed", pkg)) 86 | } 87 | } 88 | 89 | return python3, nil 90 | } 91 | 92 | func Command(name string, arg ...string) *exec.Cmd { 93 | cmd := exec.Command(name, arg...) 94 | cmd.SysProcAttr = &syscall.SysProcAttr{ 95 | HideWindow: true, 96 | } 97 | return cmd 98 | } 99 | 100 | func KillProcessesByGpid(cmd *exec.Cmd) error { 101 | killCmd := Command("taskkill", "/T", "/F", "/PID ", strconv.Itoa(cmd.Process.Pid)) 102 | return killCmd.Run() 103 | } 104 | 105 | func initShellExec(shellString string) *exec.Cmd { 106 | // cmd /C shellString 107 | return exec.Command("cmd", "/C", shellString) 108 | } 109 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package funplugin 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/httprunner/funplugin/fungo" 11 | "github.com/httprunner/funplugin/myexec" 12 | ) 13 | 14 | var ( 15 | logger = fungo.Logger 16 | ) 17 | 18 | type IPlugin interface { 19 | Type() string // get plugin type 20 | Path() string // get plugin file path 21 | Has(funcName string) bool // check if plugin has function 22 | Call(funcName string, args ...interface{}) (interface{}, error) // call function 23 | Quit() error // quit plugin 24 | StartHeartbeat() // heartbeat to keep the plugin alive 25 | } 26 | 27 | type langType string 28 | 29 | const ( 30 | langTypeGo langType = "go" 31 | langTypePython langType = "py" 32 | langTypeJava langType = "java" 33 | ) 34 | 35 | type pluginOption struct { 36 | debugLogger bool // whether set log level to DEBUG 37 | logFile string // specify log file path 38 | disableLogTime bool // whether disable log time 39 | langType langType // go or py 40 | python3 string // python3 path with funppy dependency 41 | } 42 | 43 | type Option func(*pluginOption) 44 | 45 | func WithDebugLogger(debug bool) Option { 46 | return func(o *pluginOption) { 47 | o.debugLogger = debug 48 | } 49 | } 50 | 51 | func WithLogFile(logFile string) Option { 52 | return func(o *pluginOption) { 53 | o.logFile = logFile 54 | } 55 | } 56 | 57 | func WithDisableTime(disable bool) Option { 58 | return func(o *pluginOption) { 59 | o.disableLogTime = disable 60 | } 61 | } 62 | 63 | func WithPython3(python3 string) Option { 64 | return func(o *pluginOption) { 65 | o.python3 = python3 66 | } 67 | } 68 | 69 | // Init initializes plugin with plugin path 70 | func Init(path string, options ...Option) (plugin IPlugin, err error) { 71 | option := &pluginOption{} 72 | for _, o := range options { 73 | o(option) 74 | } 75 | 76 | // init logger 77 | logLevel := hclog.Info 78 | if option.debugLogger { 79 | logLevel = hclog.Debug 80 | } 81 | logger = fungo.InitLogger( 82 | logLevel, option.logFile, option.disableLogTime) 83 | 84 | logger.Info("init plugin", "path", path) 85 | 86 | // priority: hashicorp plugin > go plugin 87 | ext := filepath.Ext(path) 88 | switch ext { 89 | case ".bin": 90 | // found hashicorp go plugin file 91 | option.langType = langTypeGo 92 | return newHashicorpPlugin(path, option) 93 | case ".py": 94 | // found hashicorp python plugin file 95 | if option.python3 == "" { 96 | // create python3 venv with funppy if python3 not specified 97 | option.python3, err = myexec.EnsurePython3Venv("", "funppy") 98 | if err != nil { 99 | logger.Error("prepare python3 funppy venv failed", "error", err) 100 | return nil, errors.Wrap(err, 101 | "miss python3, create python3 funppy venv failed") 102 | } 103 | } 104 | option.langType = langTypePython 105 | return newHashicorpPlugin(path, option) 106 | case ".so": 107 | // found go plugin file 108 | return newGoPlugin(path) 109 | default: 110 | logger.Error("invalid plugin path", "path", path, "error", err) 111 | return nil, fmt.Errorf("unsupported plugin type: %s", ext) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /fungo/utils.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // CallFunc calls function with arguments 10 | func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { 11 | argumentsValue, err := convertArgs(fn, args...) 12 | if err != nil { 13 | logger.Error("convert arguments failed", "error", err) 14 | return nil, err 15 | } 16 | return call(fn, argumentsValue) 17 | } 18 | 19 | func convertArgs(fn reflect.Value, args ...interface{}) ([]reflect.Value, error) { 20 | fnArgsNum := fn.Type().NumIn() 21 | 22 | // function arguments should match exactly if function's last argument is not slice 23 | if len(args) != fnArgsNum && (fnArgsNum == 0 || fn.Type().In(fnArgsNum-1).Kind() != reflect.Slice) { 24 | return nil, fmt.Errorf("function expect %d arguments, but got %d", fnArgsNum, len(args)) 25 | } 26 | 27 | argumentsValue := make([]reflect.Value, len(args)) 28 | for index := 0; index < len(args); index++ { 29 | argument := args[index] 30 | if argument == nil { 31 | argumentsValue[index] = reflect.Zero(fn.Type().In(index)) 32 | continue 33 | } 34 | 35 | argumentValue := reflect.ValueOf(argument) 36 | actualArgumentType := reflect.TypeOf(argument) 37 | 38 | var expectArgumentType reflect.Type 39 | if (index == fnArgsNum-1 && fn.Type().In(fnArgsNum-1).Kind() == reflect.Slice) || index > fnArgsNum-1 { 40 | // last fn argument is slice 41 | expectArgumentType = fn.Type().In(fnArgsNum - 1).Elem() // slice element type 42 | 43 | // last argument is also slice, e.g. []int 44 | if actualArgumentType.Kind() == reflect.Slice { 45 | if actualArgumentType.Elem() != expectArgumentType { 46 | err := fmt.Errorf("function argument %d's slice element type is not match, expect %v, actual %v", 47 | index, expectArgumentType, actualArgumentType) 48 | return nil, err 49 | } 50 | argumentsValue[index] = argumentValue 51 | continue 52 | } 53 | } else { 54 | expectArgumentType = fn.Type().In(index) 55 | } 56 | 57 | // type match 58 | if expectArgumentType == actualArgumentType { 59 | argumentsValue[index] = argumentValue 60 | continue 61 | } 62 | 63 | // type not match, check if convertible 64 | if !actualArgumentType.ConvertibleTo(expectArgumentType) { 65 | // function argument type not match and not convertible 66 | err := fmt.Errorf("function argument %d's type is neither match nor convertible, expect %v, actual %v", 67 | index, expectArgumentType, actualArgumentType) 68 | return nil, err 69 | } 70 | // convert argument to expect type 71 | argumentsValue[index] = argumentValue.Convert(expectArgumentType) 72 | } 73 | return argumentsValue, nil 74 | } 75 | 76 | func call(fn reflect.Value, args []reflect.Value) (interface{}, error) { 77 | resultValues := fn.Call(args) 78 | if resultValues == nil { 79 | // no returns 80 | return nil, nil 81 | } else if len(resultValues) == 2 { 82 | // return two arguments: interface{}, error 83 | if resultValues[1].Interface() != nil { 84 | return resultValues[0].Interface(), resultValues[1].Interface().(error) 85 | } else { 86 | return resultValues[0].Interface(), nil 87 | } 88 | } else if len(resultValues) == 1 { 89 | // return one argument 90 | if err, ok := resultValues[0].Interface().(error); ok { 91 | // return error 92 | return nil, err 93 | } else { 94 | // return interface{} 95 | return resultValues[0].Interface(), nil 96 | } 97 | } else { 98 | // return more than 2 arguments, unexpected 99 | err := fmt.Errorf("function should return at most 2 values") 100 | return nil, err 101 | } 102 | } 103 | 104 | // ConvertCommonName returns name which deleted "_" and converted capital letter to their lower case 105 | func ConvertCommonName(name string) string { 106 | return strings.ToLower(strings.Replace(name, "_", "", -1)) 107 | } 108 | -------------------------------------------------------------------------------- /fungo/grpc.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | "github.com/pkg/errors" 8 | "google.golang.org/grpc" 9 | 10 | "github.com/httprunner/funplugin/fungo/protoGen" 11 | jsoniter "github.com/json-iterator/go" 12 | ) 13 | 14 | // replace with third-party json library to improve performance 15 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 16 | 17 | // functionGRPCClient runs on the host side, it implements FuncCaller interface 18 | type functionGRPCClient struct { 19 | client protoGen.DebugTalkClient 20 | } 21 | 22 | func (m *functionGRPCClient) GetNames() ([]string, error) { 23 | logger.Debug("gRPC_client GetNames() start") 24 | resp, err := m.client.GetNames(context.Background(), &protoGen.Empty{}) 25 | if err != nil { 26 | logger.Error("gRPC_client GetNames() failed", "error", err) 27 | return nil, err 28 | } 29 | logger.Debug("gRPC_client GetNames() success") 30 | return resp.Names, nil 31 | } 32 | 33 | func (m *functionGRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { 34 | logger.Info("gRPC_client Call() start", "funcName", funcName, "funcArgs", funcArgs) 35 | 36 | funcArgBytes, err := json.Marshal(funcArgs) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "failed to marshal Call() funcArgs") 39 | } 40 | req := &protoGen.CallRequest{ 41 | Name: funcName, 42 | Args: funcArgBytes, 43 | } 44 | 45 | response, err := m.client.Call(context.Background(), req) 46 | if err != nil { 47 | logger.Error("gRPC_client Call() failed", 48 | "funcName", funcName, 49 | "funcArgs", funcArgs, 50 | "error", err, 51 | ) 52 | return nil, err 53 | } 54 | 55 | var resp interface{} 56 | err = json.Unmarshal(response.Value, &resp) 57 | if err != nil { 58 | return nil, errors.Wrap(err, "failed to unmarshal Call() response") 59 | } 60 | logger.Info("gRPC_client Call() success", "result", resp) 61 | return resp, nil 62 | } 63 | 64 | // Here is the gRPC server that functionGRPCClient talks to. 65 | type functionGRPCServer struct { 66 | protoGen.UnimplementedDebugTalkServer 67 | Impl IFuncCaller 68 | } 69 | 70 | func (m *functionGRPCServer) GetNames(ctx context.Context, req *protoGen.Empty) (*protoGen.GetNamesResponse, error) { 71 | logger.Debug("gRPC_server GetNames() start") 72 | v, err := m.Impl.GetNames() 73 | if err != nil { 74 | logger.Error("gRPC_server GetNames() failed", "error", err) 75 | return nil, err 76 | } 77 | logger.Debug("gRPC_server GetNames() success") 78 | return &protoGen.GetNamesResponse{Names: v}, nil 79 | } 80 | 81 | func (m *functionGRPCServer) Call(ctx context.Context, req *protoGen.CallRequest) (*protoGen.CallResponse, error) { 82 | logger.Debug("gRPC_server Call() start") 83 | 84 | var funcArgs []interface{} 85 | if err := json.Unmarshal(req.Args, &funcArgs); err != nil { 86 | return nil, errors.Wrap(err, "failed to unmarshal Call() funcArgs") 87 | } 88 | 89 | v, err := m.Impl.Call(req.Name, funcArgs...) 90 | if err != nil { 91 | logger.Error("gRPC_server Call() failed", "req", req, "error", err) 92 | return nil, err 93 | } 94 | 95 | value, err := json.Marshal(v) 96 | if err != nil { 97 | return nil, errors.Wrap(err, "failed to marshal Call() response") 98 | } 99 | logger.Debug("gRPC_server Call() success") 100 | return &protoGen.CallResponse{Value: value}, nil 101 | } 102 | 103 | // GRPCPlugin implements hashicorp's plugin.GRPCPlugin. 104 | type GRPCPlugin struct { 105 | plugin.Plugin 106 | Impl IFuncCaller 107 | } 108 | 109 | func (p *GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 110 | protoGen.RegisterDebugTalkServer(s, &functionGRPCServer{Impl: p.Impl}) 111 | return nil 112 | } 113 | 114 | func (p *GRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 115 | return &functionGRPCClient{client: protoGen.NewDebugTalkClient(c)}, nil 116 | } 117 | -------------------------------------------------------------------------------- /hashicorp_plugin_test.go: -------------------------------------------------------------------------------- 1 | package funplugin 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/httprunner/funplugin/fungo" 10 | "github.com/httprunner/funplugin/myexec" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var pluginBinPath = "fungo/examples/debugtalk.bin" 15 | 16 | func buildHashicorpGoPlugin() { 17 | logger.Info("[setup test] build hashicorp go plugin", "path", pluginBinPath) 18 | err := myexec.RunCommand("go", "build", 19 | "-o", pluginBinPath, 20 | "fungo/examples/hashicorp.go", "fungo/examples/debugtalk.go") 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | func removeHashicorpGoPlugin() { 27 | logger.Info("[teardown test] remove hashicorp plugin", "path", pluginBinPath) 28 | os.Remove(pluginBinPath) 29 | } 30 | 31 | func TestHashicorpGRPCGoPlugin(t *testing.T) { 32 | buildHashicorpGoPlugin() 33 | defer removeHashicorpGoPlugin() 34 | 35 | logFile := filepath.Join("docs", "logs", "hashicorp_grpc_go.log") 36 | plugin, err := Init("fungo/examples/debugtalk.bin", 37 | WithDebugLogger(true), 38 | WithLogFile(logFile), 39 | WithDisableTime(true)) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer plugin.Quit() 44 | 45 | assertPlugin(t, plugin) 46 | } 47 | 48 | func TestHashicorpRPCGoPlugin(t *testing.T) { 49 | buildHashicorpGoPlugin() 50 | defer removeHashicorpGoPlugin() 51 | 52 | logFile := filepath.Join("docs", "logs", "hashicorp_rpc_go.log") 53 | os.Setenv(fungo.PluginTypeEnvName, "rpc") 54 | plugin, err := Init("fungo/examples/debugtalk.bin", 55 | WithDebugLogger(true), 56 | WithLogFile(logFile), 57 | WithDisableTime(true)) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | defer plugin.Quit() 62 | 63 | assertPlugin(t, plugin) 64 | } 65 | 66 | func TestHashicorpPythonPluginWithVenv(t *testing.T) { 67 | dir, err := os.MkdirTemp(os.TempDir(), "prefix") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | defer os.RemoveAll(dir) 72 | 73 | venvDir := filepath.Join(dir, ".hrp", "venv") 74 | err = myexec.RunCommand("python3", "-m", "venv", venvDir) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | var python3 string 80 | if runtime.GOOS == "windows" { 81 | python3 = filepath.Join(venvDir, "Scripts", "python3.exe") 82 | } else { 83 | python3 = filepath.Join(venvDir, "bin", "python3") 84 | } 85 | 86 | err = myexec.RunCommand(python3, "-m", "pip", "install", "funppy") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | plugin, err := Init("funppy/examples/debugtalk.py", WithPython3(python3)) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | defer plugin.Quit() 96 | 97 | assertPlugin(t, plugin) 98 | } 99 | 100 | func assertPlugin(t *testing.T, plugin IPlugin) { 101 | var err error 102 | if !assert.True(t, plugin.Has("sum_ints")) { 103 | t.Fail() 104 | } 105 | if !assert.True(t, plugin.Has("concatenate")) { 106 | t.Fail() 107 | } 108 | 109 | var v2 interface{} 110 | v2, err = plugin.Call("sum_ints", 1, 2, 3, 4) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | if !assert.EqualValues(t, 10, v2) { 115 | t.Fail() 116 | } 117 | v2, err = plugin.Call("sum_two_int", 1, 2) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | if !assert.EqualValues(t, 3, v2) { 122 | t.Fail() 123 | } 124 | v2, err = plugin.Call("sum", 1, 2, 3.4, 5) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | if !assert.Equal(t, 11.4, v2) { 129 | t.Fail() 130 | } 131 | 132 | var v3 interface{} 133 | v3, err = plugin.Call("sum_two_string", "a", "b") 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if !assert.Equal(t, "ab", v3) { 138 | t.Fail() 139 | } 140 | v3, err = plugin.Call("sum_strings", "a", "b", "c") 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | if !assert.Equal(t, "abc", v3) { 145 | t.Fail() 146 | } 147 | 148 | v3, err = plugin.Call("concatenate", "a", 2, "c", 3.4) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | if !assert.Equal(t, "a2c3.4", v3) { 153 | t.Fail() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /funppy/debugtalk_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | from funppy import debugtalk_pb2 as debugtalk__pb2 6 | 7 | 8 | class DebugTalkStub(object): 9 | """Missing associated documentation comment in .proto file.""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.GetNames = channel.unary_unary( 18 | '/proto.DebugTalk/GetNames', 19 | request_serializer=debugtalk__pb2.Empty.SerializeToString, 20 | response_deserializer=debugtalk__pb2.GetNamesResponse.FromString, 21 | ) 22 | self.Call = channel.unary_unary( 23 | '/proto.DebugTalk/Call', 24 | request_serializer=debugtalk__pb2.CallRequest.SerializeToString, 25 | response_deserializer=debugtalk__pb2.CallResponse.FromString, 26 | ) 27 | 28 | 29 | class DebugTalkServicer(object): 30 | """Missing associated documentation comment in .proto file.""" 31 | 32 | def GetNames(self, request, context): 33 | """Missing associated documentation comment in .proto file.""" 34 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 35 | context.set_details('Method not implemented!') 36 | raise NotImplementedError('Method not implemented!') 37 | 38 | def Call(self, request, context): 39 | """Missing associated documentation comment in .proto file.""" 40 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 41 | context.set_details('Method not implemented!') 42 | raise NotImplementedError('Method not implemented!') 43 | 44 | 45 | def add_DebugTalkServicer_to_server(servicer, server): 46 | rpc_method_handlers = { 47 | 'GetNames': grpc.unary_unary_rpc_method_handler( 48 | servicer.GetNames, 49 | request_deserializer=debugtalk__pb2.Empty.FromString, 50 | response_serializer=debugtalk__pb2.GetNamesResponse.SerializeToString, 51 | ), 52 | 'Call': grpc.unary_unary_rpc_method_handler( 53 | servicer.Call, 54 | request_deserializer=debugtalk__pb2.CallRequest.FromString, 55 | response_serializer=debugtalk__pb2.CallResponse.SerializeToString, 56 | ), 57 | } 58 | generic_handler = grpc.method_handlers_generic_handler( 59 | 'proto.DebugTalk', rpc_method_handlers) 60 | server.add_generic_rpc_handlers((generic_handler,)) 61 | 62 | 63 | # This class is part of an EXPERIMENTAL API. 64 | class DebugTalk(object): 65 | """Missing associated documentation comment in .proto file.""" 66 | 67 | @staticmethod 68 | def GetNames(request, 69 | target, 70 | options=(), 71 | channel_credentials=None, 72 | call_credentials=None, 73 | insecure=False, 74 | compression=None, 75 | wait_for_ready=None, 76 | timeout=None, 77 | metadata=None): 78 | return grpc.experimental.unary_unary(request, target, '/proto.DebugTalk/GetNames', 79 | debugtalk__pb2.Empty.SerializeToString, 80 | debugtalk__pb2.GetNamesResponse.FromString, 81 | options, channel_credentials, 82 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 83 | 84 | @staticmethod 85 | def Call(request, 86 | target, 87 | options=(), 88 | channel_credentials=None, 89 | call_credentials=None, 90 | insecure=False, 91 | compression=None, 92 | wait_for_ready=None, 93 | timeout=None, 94 | metadata=None): 95 | return grpc.experimental.unary_unary(request, target, '/proto.DebugTalk/Call', 96 | debugtalk__pb2.CallRequest.SerializeToString, 97 | debugtalk__pb2.CallResponse.FromString, 98 | options, channel_credentials, 99 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FunPlugin 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/httprunner/funplugin.svg)](https://pkg.go.dev/github.com/httprunner/funplugin) 4 | [![Github Actions](https://github.com/httprunner/funplugin/actions/workflows/unittest.yml/badge.svg)](https://github.com/httprunner/funplugin/actions) 5 | [![codecov](https://codecov.io/gh/httprunner/funplugin/branch/main/graph/badge.svg?token=DW3K2R1PNC)](https://codecov.io/gh/httprunner/funplugin) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/httprunner/funplugin)](https://goreportcard.com/report/github.com/httprunner/funplugin) 7 | 8 | ## What is FunPlugin? 9 | 10 | `FunPlugin` is short for function plugin, and I hope you can have fun using this plugin too. 11 | 12 | This plugin project comes from the requirements of [HttpRunner+], because we need to implement some dynamic calculations or custom logic processing in YAML/JSON test cases. If you have used [HttpRunner] before, you must be impressed by `debugtalk.py`, which allows us to customize functions very easily and reference them in plain text test cases. 13 | 14 | As the HttpRunner project evolves, we expect users to have more choices besides Python when writing custom functions, such as Golang, Java, Node, C++, C#, etc. 15 | 16 | `FunPlugin` achieves this goal well and grows into an independent project that not only serves HttpRunner+, but can also be easily integrated into other golang projects. 17 | 18 | ## How to use FunPlugin? 19 | 20 | `FunPlugin` is mainly based on [hashicorp plugin], which is a golang plugin system over RPC. It supports serving plugins via `gRPC`, which means plugins can be written in any language. You can find the official programming languages supported by grpc [here][grpc-lang]. 21 | 22 | Integrating `FunPlugin` is very simple. You only need to focus on two parts. 23 | 24 | - client side: integrate FunPlugin into your golang project, call plugin functions at will. 25 | - plugin side: write plugin functions in your favorite language and build them to plugin binary 26 | 27 | ### client call 28 | 29 | FunPlugin has a very concise golang API that can be easily integrated into golang projects. 30 | 31 | 1, use `Init` to initialize plugin via plugin path. 32 | 33 | ```go 34 | func Init(path string, options ...Option) (plugin IPlugin, err error) 35 | ``` 36 | 37 | - path: built plugin file path 38 | - options: specify extra plugin options 39 | - `WithDebugLogger(debug bool)`: whether to print debug level logs in plugin process 40 | - `WithLogFile(logFile string)`: specify log file path 41 | - `WithDisableTime(disable bool)`: whether disable log time 42 | - `WithPython3(python3 string)`: specify custom python3 path 43 | 44 | 2, call plugin API to deal with plugin functions. 45 | 46 | If the specified plugin path is valid, you will get a plugin instance conforming to the `IPlugin` interface. 47 | 48 | ```go 49 | type IPlugin interface { 50 | Type() string 51 | Has(funcName string) bool 52 | Call(funcName string, args ...interface{}) (interface{}, error) 53 | Quit() error 54 | } 55 | ``` 56 | 57 | - Type: returns plugin type, current available types are `go-plugin`/`hashicorp-rpc-go`/`hashicorp-grpc-go`/`hashicorp-grpc-py` 58 | - Has: check if plugin has a function 59 | - Call: call function with function name and arguments 60 | - Quit: quit plugin 61 | 62 | You can reference [hashicorp_plugin_test.go] and [go_plugin_test.go] as examples. 63 | 64 | ### plugin server 65 | 66 | In `RPC` architecture, plugins can be considered as servers. You can write plugin functions in your favorite language and then build them to a binary file. When the client `Init` the plugin file path, it starts the plugin as a server and they can then communicates via RPC. 67 | 68 | Currently, `FunPlugin` supports 3 different plugins via RPC. You can check their documentation for more details. 69 | 70 | - [x] [Golang plugin over gRPC][go-grpc-plugin], built as `xxx.bin` (recommended) 71 | - [x] [Golang plugin over net/rpc][go-rpc-plugin], built as `xxx.bin` 72 | - [x] [Python plugin over gRPC][python-grpc-plugin], no need to build, just name it with `xxx.py` 73 | 74 | You are welcome to contribute more plugins in other languages. 75 | 76 | - [ ] Java plugin over gRPC 77 | - [ ] Node plugin over gRPC 78 | - [ ] C++ plugin over gRPC 79 | - [ ] C# plugin over gRPC 80 | - [ ] [etc.][grpc-lang] 81 | 82 | Finally, `FunPlugin` also supports writing plugin function with the official [go plugin]. However, this solution has a number of limitations. You can check this [document][go-plugin] for more details. 83 | 84 | 85 | [HttpRunner+]: https://github.com/httprunner/hrp 86 | [HttpRunner]: https://github.com/httprunner/httprunner 87 | [hashicorp plugin]: https://github.com/hashicorp/go-plugin 88 | [grpc-lang]: https://www.grpc.io/docs/languages/ 89 | [go plugin]: https://pkg.go.dev/plugin 90 | [examples/plugin/]: ../examples/plugin/ 91 | [examples/plugin/debugtalk.go]: ../examples/plugin/debugtalk.go 92 | [hashicorp_plugin_test.go]: hashicorp_plugin_test.go 93 | [go_plugin_test.go]: go_plugin_test.go 94 | [go-grpc-plugin]: docs/go-grpc-plugin.md 95 | [go-rpc-plugin]: docs/go-rpc-plugin.md 96 | [python-grpc-plugin]: docs/python-grpc-plugin.md 97 | [go-plugin]: docs/go-plugin.md 98 | -------------------------------------------------------------------------------- /fungo/protoGen/debugtalk_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.19.4 5 | // source: proto/debugtalk.proto 6 | 7 | package protoGen 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // DebugTalkClient is the client API for DebugTalk service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type DebugTalkClient interface { 25 | GetNames(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetNamesResponse, error) 26 | Call(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallResponse, error) 27 | } 28 | 29 | type debugTalkClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewDebugTalkClient(cc grpc.ClientConnInterface) DebugTalkClient { 34 | return &debugTalkClient{cc} 35 | } 36 | 37 | func (c *debugTalkClient) GetNames(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetNamesResponse, error) { 38 | out := new(GetNamesResponse) 39 | err := c.cc.Invoke(ctx, "/proto.DebugTalk/GetNames", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *debugTalkClient) Call(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallResponse, error) { 47 | out := new(CallResponse) 48 | err := c.cc.Invoke(ctx, "/proto.DebugTalk/Call", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | // DebugTalkServer is the server API for DebugTalk service. 56 | // All implementations must embed UnimplementedDebugTalkServer 57 | // for forward compatibility 58 | type DebugTalkServer interface { 59 | GetNames(context.Context, *Empty) (*GetNamesResponse, error) 60 | Call(context.Context, *CallRequest) (*CallResponse, error) 61 | mustEmbedUnimplementedDebugTalkServer() 62 | } 63 | 64 | // UnimplementedDebugTalkServer must be embedded to have forward compatible implementations. 65 | type UnimplementedDebugTalkServer struct { 66 | } 67 | 68 | func (UnimplementedDebugTalkServer) GetNames(context.Context, *Empty) (*GetNamesResponse, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method GetNames not implemented") 70 | } 71 | func (UnimplementedDebugTalkServer) Call(context.Context, *CallRequest) (*CallResponse, error) { 72 | return nil, status.Errorf(codes.Unimplemented, "method Call not implemented") 73 | } 74 | func (UnimplementedDebugTalkServer) mustEmbedUnimplementedDebugTalkServer() {} 75 | 76 | // UnsafeDebugTalkServer may be embedded to opt out of forward compatibility for this service. 77 | // Use of this interface is not recommended, as added methods to DebugTalkServer will 78 | // result in compilation errors. 79 | type UnsafeDebugTalkServer interface { 80 | mustEmbedUnimplementedDebugTalkServer() 81 | } 82 | 83 | func RegisterDebugTalkServer(s grpc.ServiceRegistrar, srv DebugTalkServer) { 84 | s.RegisterService(&DebugTalk_ServiceDesc, srv) 85 | } 86 | 87 | func _DebugTalk_GetNames_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 88 | in := new(Empty) 89 | if err := dec(in); err != nil { 90 | return nil, err 91 | } 92 | if interceptor == nil { 93 | return srv.(DebugTalkServer).GetNames(ctx, in) 94 | } 95 | info := &grpc.UnaryServerInfo{ 96 | Server: srv, 97 | FullMethod: "/proto.DebugTalk/GetNames", 98 | } 99 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 100 | return srv.(DebugTalkServer).GetNames(ctx, req.(*Empty)) 101 | } 102 | return interceptor(ctx, in, info, handler) 103 | } 104 | 105 | func _DebugTalk_Call_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(CallRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(DebugTalkServer).Call(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: "/proto.DebugTalk/Call", 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(DebugTalkServer).Call(ctx, req.(*CallRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | // DebugTalk_ServiceDesc is the grpc.ServiceDesc for DebugTalk service. 124 | // It's only intended for direct use with grpc.RegisterService, 125 | // and not to be introspected or modified (even as a copy) 126 | var DebugTalk_ServiceDesc = grpc.ServiceDesc{ 127 | ServiceName: "proto.DebugTalk", 128 | HandlerType: (*DebugTalkServer)(nil), 129 | Methods: []grpc.MethodDesc{ 130 | { 131 | MethodName: "GetNames", 132 | Handler: _DebugTalk_GetNames_Handler, 133 | }, 134 | { 135 | MethodName: "Call", 136 | Handler: _DebugTalk_Call_Handler, 137 | }, 138 | }, 139 | Streams: []grpc.StreamDesc{}, 140 | Metadata: "proto/debugtalk.proto", 141 | } 142 | -------------------------------------------------------------------------------- /hashicorp_plugin.go: -------------------------------------------------------------------------------- 1 | package funplugin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "sync" 8 | "time" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | 12 | "github.com/hashicorp/go-plugin" 13 | "github.com/pkg/errors" 14 | 15 | "github.com/httprunner/funplugin/fungo" 16 | ) 17 | 18 | type rpcType string 19 | 20 | const ( 21 | rpcTypeRPC rpcType = "rpc" // go net/rpc 22 | rpcTypeGRPC rpcType = "grpc" // default 23 | ) 24 | 25 | func (t rpcType) String() string { 26 | return string(t) 27 | } 28 | 29 | // hashicorpPlugin implements hashicorp/go-plugin 30 | type hashicorpPlugin struct { 31 | client *plugin.Client 32 | rpcType rpcType 33 | funcCaller fungo.IFuncCaller 34 | cachedFunctions sync.Map // cache loaded functions to improve performance, key is function name, value is bool 35 | path string // plugin file path 36 | option *pluginOption 37 | } 38 | 39 | func newHashicorpPlugin(path string, option *pluginOption) (*hashicorpPlugin, error) { 40 | p := &hashicorpPlugin{ 41 | path: path, 42 | option: option, 43 | } 44 | 45 | // plugin type, grpc or rpc 46 | p.rpcType = rpcType(os.Getenv(fungo.PluginTypeEnvName)) 47 | if p.rpcType != rpcTypeRPC { 48 | p.rpcType = rpcTypeGRPC // default 49 | } 50 | // logger 51 | logger = logger.ResetNamed(fmt.Sprintf("hc-%v-%v", p.rpcType, p.option.langType)) 52 | 53 | // 失败则继续尝试,连续三次失败则返回错误 54 | err := p.startPlugin() 55 | if err == nil { 56 | return p, err 57 | } 58 | logger.Info("load hashicorp go plugin success", "path", path) 59 | 60 | return nil, err 61 | } 62 | 63 | func (p *hashicorpPlugin) Type() string { 64 | return fmt.Sprintf("hashicorp-%s-%v", p.rpcType, p.option.langType) 65 | } 66 | 67 | func (p *hashicorpPlugin) Path() string { 68 | return p.path 69 | } 70 | 71 | func (p *hashicorpPlugin) Has(funcName string) bool { 72 | logger.Debug("check if plugin has function", "funcName", funcName) 73 | flag, ok := p.cachedFunctions.Load(funcName) 74 | if ok { 75 | return flag.(bool) 76 | } 77 | 78 | funcNames, err := p.funcCaller.GetNames() 79 | if err != nil { 80 | return false 81 | } 82 | 83 | for _, name := range funcNames { 84 | if name == funcName { 85 | p.cachedFunctions.Store(funcName, true) // cache as exists 86 | return true 87 | } 88 | } 89 | 90 | p.cachedFunctions.Store(funcName, false) // cache as not exists 91 | return false 92 | } 93 | 94 | func (p *hashicorpPlugin) Call(funcName string, args ...interface{}) (interface{}, error) { 95 | return p.funcCaller.Call(funcName, args...) 96 | } 97 | 98 | func (p *hashicorpPlugin) StartHeartbeat() { 99 | ticker := time.NewTicker(15 * time.Second) 100 | defer ticker.Stop() 101 | var err error 102 | 103 | for range ticker.C { 104 | // Check the client connection status 105 | logger.Info("heartbreak......") 106 | if p.client.Exited() { 107 | logger.Error(fmt.Sprintf("plugin exited, restarting...")) 108 | err = p.startPlugin() 109 | if err != nil { 110 | break 111 | } 112 | } 113 | } 114 | } 115 | 116 | func (p *hashicorpPlugin) startPlugin() error { 117 | var cmd *exec.Cmd 118 | if p.option.langType == langTypePython { 119 | // hashicorp python plugin 120 | cmd = exec.Command(p.option.python3, p.path) 121 | // hashicorp python plugin only supports gRPC 122 | p.rpcType = rpcTypeGRPC 123 | } else { 124 | // hashicorp go plugin 125 | cmd = exec.Command(p.path) 126 | // hashicorp go plugin supports grpc and rpc 127 | p.rpcType = rpcType(os.Getenv(fungo.PluginTypeEnvName)) 128 | if p.rpcType != rpcTypeRPC { 129 | p.rpcType = rpcTypeGRPC // default 130 | } 131 | } 132 | cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", fungo.PluginTypeEnvName, p.rpcType)) 133 | 134 | var err error 135 | maxRetryCount := 3 136 | for i := 0; i < maxRetryCount; i++ { 137 | err = p.tryStartPlugin(cmd, logger) 138 | if err == nil { 139 | return nil 140 | } 141 | time.Sleep(time.Second * time.Duration(i*i)) // sleep temporarily before next try 142 | } 143 | logger.Error("failed to start plugin after max retries") 144 | return errors.Wrap(err, "failed to start plugin after max retries") 145 | } 146 | 147 | func (p *hashicorpPlugin) tryStartPlugin(cmd *exec.Cmd, logger hclog.Logger) error { 148 | // launch the plugin process 149 | p.client = plugin.NewClient(&plugin.ClientConfig{ 150 | HandshakeConfig: fungo.HandshakeConfig, 151 | Plugins: map[string]plugin.Plugin{ 152 | rpcTypeRPC.String(): &fungo.RPCPlugin{}, 153 | rpcTypeGRPC.String(): &fungo.GRPCPlugin{}, 154 | }, 155 | Cmd: cmd, 156 | Logger: logger, 157 | AllowedProtocols: []plugin.Protocol{ 158 | plugin.ProtocolNetRPC, 159 | plugin.ProtocolGRPC, 160 | }, 161 | }) 162 | 163 | // Connect via RPC/gRPC 164 | rpcClient, err := p.client.Client() 165 | if err != nil { 166 | return errors.Wrap(err, fmt.Sprintf("connect %s plugin failed", p.rpcType)) 167 | } 168 | 169 | // Request the plugin 170 | raw, err := rpcClient.Dispense(p.rpcType.String()) 171 | if err != nil { 172 | return errors.Wrap(err, fmt.Sprintf("request %s plugin failed", p.rpcType)) 173 | } 174 | 175 | // We should have a Function now! This feels like a normal interface 176 | // implementation but is in fact over an RPC connection. 177 | p.funcCaller = raw.(fungo.IFuncCaller) 178 | 179 | p.cachedFunctions = sync.Map{} 180 | 181 | return nil 182 | } 183 | 184 | func (p *hashicorpPlugin) Quit() error { 185 | // kill hashicorp plugin process 186 | logger.Info("quit hashicorp plugin process") 187 | p.client.Kill() 188 | return fungo.CloseLogFile() 189 | } 190 | -------------------------------------------------------------------------------- /docs/logs/hashicorp_rpc_go.log: -------------------------------------------------------------------------------- 1 | [INFO] fungo: set plugin log level: level=debug 2 | [INFO] fungo: init plugin: path=fungo/examples/debugtalk.bin 3 | [INFO] hc-rpc-go: launch the plugin process 4 | [DEBUG] hc-rpc-go: starting plugin: path=fungo/examples/debugtalk.bin args=["fungo/examples/debugtalk.bin"] 5 | [DEBUG] hc-rpc-go: plugin started: path=fungo/examples/debugtalk.bin pid=21194 6 | [DEBUG] hc-rpc-go: waiting for RPC address: path=fungo/examples/debugtalk.bin 7 | [DEBUG] hc-rpc-go.debugtalk.bin: 2023/08/20 14:58:04 plugin init function called 8 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_ints 9 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_two_int 10 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum 11 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_two_string 12 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_strings 13 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=concatenate 14 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=setup_hook_example 15 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=teardown_hook_example 16 | [INFO] hc-rpc-go.debugtalk.bin: [INFO] fungo: start plugin server in RPC mode 17 | [DEBUG] hc-rpc-go: using plugin: version=1 18 | [DEBUG] hc-rpc-go.debugtalk.bin: plugin address: network=unix address=/var/folders/nm/6prc3p4s2tg_27_3fwfv22vh0000gp/T/plugin158499432 timestamp="2023-08-20T14:58:04.565+0800" 19 | [INFO] hc-rpc-go: load hashicorp go plugin success: path=fungo/examples/debugtalk.bin 20 | [DEBUG] hc-rpc-go: check if plugin has function: funcName=sum_ints 21 | [DEBUG] fungo: rpc_client GetNames() start 22 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server GetNames() start 23 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: get registered plugin functions: names=["sum", "sumtwostring", "concatenate", "setup_hook_example", "sum_two_int", "sumstrings", "teardown_hook_example", "teardownhookexample", "sum_ints", "sumints", "sumtwoint", "sum_two_string", "sum_strings", "setuphookexample"] 24 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server GetNames() success 25 | [DEBUG] fungo: rpc_client GetNames() success 26 | [DEBUG] hc-rpc-go: check if plugin has function: funcName=concatenate 27 | [DEBUG] fungo: rpc_client GetNames() start 28 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server GetNames() start 29 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: get registered plugin functions: names=["sum", "sumtwostring", "concatenate", "setup_hook_example", "sum_two_int", "sumstrings", "teardown_hook_example", "teardownhookexample", "sum_ints", "sumints", "sumtwoint", "sum_two_string", "sum_strings", "setuphookexample"] 30 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server GetNames() success 31 | [DEBUG] fungo: rpc_client GetNames() success 32 | [INFO] fungo: rpc_client Call() start: funcName=sum_ints funcArgs=[1, 2, 3, 4] 33 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 34 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_ints args=[1, 2, 3, 4] 35 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 36 | [INFO] fungo: rpc_client Call() success: result=10 37 | [INFO] fungo: rpc_client Call() start: funcName=sum_two_int funcArgs=[1, 2] 38 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 39 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_two_int args=[1, 2] 40 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 41 | [INFO] fungo: rpc_client Call() success: result=3 42 | [INFO] fungo: rpc_client Call() start: funcName=sum funcArgs=[1, 2, 3.4, 5] 43 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 44 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum args=[1, 2, 3.4, 5] 45 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 46 | [INFO] fungo: rpc_client Call() success: result=11.4 47 | [INFO] fungo: rpc_client Call() start: funcName=sum_two_string funcArgs=[a, b] 48 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 49 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_two_string args=[a, b] 50 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 51 | [INFO] fungo: rpc_client Call() success: result=ab 52 | [INFO] fungo: rpc_client Call() start: funcName=sum_strings funcArgs=[a, b, c] 53 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 54 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_strings args=[a, b, c] 55 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 56 | [INFO] fungo: rpc_client Call() success: result=abc 57 | [INFO] fungo: rpc_client Call() start: funcName=concatenate funcArgs=[a, 2, c, 3.4] 58 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() start 59 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=concatenate args=[a, 2, c, 3.4] 60 | [DEBUG] hc-rpc-go.debugtalk.bin: [DEBUG] fungo: rpc_server Call() success 61 | [INFO] fungo: rpc_client Call() success: result=a2c3.4 62 | [INFO] hc-rpc-go: quit hashicorp plugin process 63 | [DEBUG] hc-rpc-go.debugtalk.bin: 2023/08/20 14:58:04 [DEBUG] plugin: plugin server: accept unix /var/folders/nm/6prc3p4s2tg_27_3fwfv22vh0000gp/T/plugin158499432: use of closed network connection 64 | [INFO] hc-rpc-go: plugin process exited: path=fungo/examples/debugtalk.bin pid=21194 65 | [DEBUG] hc-rpc-go: plugin exited 66 | [INFO] fungo: close log file 67 | e 68 | -------------------------------------------------------------------------------- /docs/logs/hashicorp_grpc_go.log: -------------------------------------------------------------------------------- 1 | [INFO] fungo: set plugin log level: level=debug 2 | [INFO] fungo: init plugin: path=fungo/examples/debugtalk.bin 3 | [INFO] hc-grpc-go: launch the plugin process 4 | [DEBUG] hc-grpc-go: starting plugin: path=fungo/examples/debugtalk.bin args=["fungo/examples/debugtalk.bin"] 5 | [DEBUG] hc-grpc-go: plugin started: path=fungo/examples/debugtalk.bin pid=21171 6 | [DEBUG] hc-grpc-go: waiting for RPC address: path=fungo/examples/debugtalk.bin 7 | [DEBUG] hc-grpc-go.debugtalk.bin: 2023/08/20 14:58:03 plugin init function called 8 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_ints 9 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_two_int 10 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum 11 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_two_string 12 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=sum_strings 13 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=concatenate 14 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=setup_hook_example 15 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: register plugin function: funcName=teardown_hook_example 16 | [INFO] hc-grpc-go.debugtalk.bin: [INFO] fungo: start plugin server in gRPC mode 17 | [DEBUG] hc-grpc-go: using plugin: version=1 18 | [DEBUG] hc-grpc-go.debugtalk.bin: plugin address: network=unix address=/var/folders/nm/6prc3p4s2tg_27_3fwfv22vh0000gp/T/plugin3590521614 timestamp="2023-08-20T14:58:03.662+0800" 19 | [INFO] hc-grpc-go: load hashicorp go plugin success: path=fungo/examples/debugtalk.bin 20 | [DEBUG] hc-grpc-go: check if plugin has function: funcName=sum_ints 21 | [DEBUG] fungo: gRPC_client GetNames() start 22 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server GetNames() start 23 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: get registered plugin functions: names=["sum_two_int", "teardownhookexample", "setup_hook_example", "sumtwoint", "sumstrings", "concatenate", "setuphookexample", "teardown_hook_example", "sum_strings", "sum_ints", "sumints", "sum", "sum_two_string", "sumtwostring"] 24 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server GetNames() success 25 | [DEBUG] fungo: gRPC_client GetNames() success 26 | [DEBUG] hc-grpc-go: check if plugin has function: funcName=concatenate 27 | [DEBUG] fungo: gRPC_client GetNames() start 28 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server GetNames() start 29 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: get registered plugin functions: names=["setuphookexample", "teardown_hook_example", "sumtwoint", "sumstrings", "concatenate", "sum_two_string", "sumtwostring", "sum_strings", "sum_ints", "sumints", "sum", "sum_two_int", "teardownhookexample", "setup_hook_example"] 30 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server GetNames() success 31 | [DEBUG] fungo: gRPC_client GetNames() success 32 | [INFO] fungo: gRPC_client Call() start: funcName=sum_ints funcArgs=[1, 2, 3, 4] 33 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 34 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_ints args=[1, 2, 3, 4] 35 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 36 | [INFO] fungo: gRPC_client Call() success: result=10 37 | [INFO] fungo: gRPC_client Call() start: funcName=sum_two_int funcArgs=[1, 2] 38 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 39 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_two_int args=[1, 2] 40 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 41 | [INFO] fungo: gRPC_client Call() success: result=3 42 | [INFO] fungo: gRPC_client Call() start: funcName=sum funcArgs=[1, 2, 3.4, 5] 43 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 44 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum args=[1, 2, 3.4, 5] 45 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 46 | [INFO] fungo: gRPC_client Call() success: result=11.4 47 | [INFO] fungo: gRPC_client Call() start: funcName=sum_two_string funcArgs=[a, b] 48 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 49 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_two_string args=[a, b] 50 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 51 | [INFO] fungo: gRPC_client Call() success: result=ab 52 | [INFO] fungo: gRPC_client Call() start: funcName=sum_strings funcArgs=[a, b, c] 53 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 54 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=sum_strings args=[a, b, c] 55 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 56 | [INFO] fungo: gRPC_client Call() success: result=abc 57 | [INFO] fungo: gRPC_client Call() start: funcName=concatenate funcArgs=[a, 2, c, 3.4] 58 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() start 59 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo.func_exec: plugin function execution: funcName=concatenate args=[a, 2, c, 3.4] 60 | [DEBUG] hc-grpc-go.debugtalk.bin: [DEBUG] fungo: gRPC_server Call() success 61 | [INFO] fungo: gRPC_client Call() success: result=a2c3.4 62 | [INFO] hc-grpc-go: quit hashicorp plugin process 63 | [DEBUG] hc-grpc-go.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = error reading from server: EOF" 64 | [INFO] hc-grpc-go: plugin process exited: path=fungo/examples/debugtalk.bin pid=21171 65 | [DEBUG] hc-grpc-go: plugin exited 66 | [INFO] fungo: close log file 67 | -------------------------------------------------------------------------------- /myexec/cmd.go: -------------------------------------------------------------------------------- 1 | package myexec 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/httprunner/funplugin/fungo" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | logger = fungo.Logger 18 | PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL") 19 | PATH = os.Getenv("PATH") 20 | ) 21 | 22 | var python3Executable string = "python3" // system default python3 23 | 24 | func isPython3(python string) bool { 25 | out, err := Command(python, "--version").Output() 26 | if err != nil { 27 | return false 28 | } 29 | if strings.HasPrefix(string(out), "Python 3") { 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | // EnsurePython3Venv ensures python3 venv with specified packages 36 | // venv should be directory path of target venv 37 | func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) { 38 | // priority: specified > $HOME/.hrp/venv 39 | if venv == "" { 40 | home, err := os.UserHomeDir() 41 | if err != nil { 42 | return "", errors.Wrap(err, "get user home dir failed") 43 | } 44 | venv = filepath.Join(home, ".hrp", "venv") 45 | } 46 | python3, err = ensurePython3Venv(venv, packages...) 47 | if err != nil { 48 | return "", err 49 | } 50 | python3Executable = python3 51 | logger.Info("set python3 executable path", 52 | "Python3Executable", python3Executable) 53 | return python3, nil 54 | } 55 | 56 | func ExecPython3Command(cmdName string, args ...string) error { 57 | args = append([]string{"-m", cmdName}, args...) 58 | return RunCommand(python3Executable, args...) 59 | } 60 | 61 | func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error { 62 | out, err := Command( 63 | python3, "-c", fmt.Sprintf("\"import %s; print(%s.__version__)\"", pkgName, pkgName), 64 | ).Output() 65 | if err != nil { 66 | return fmt.Errorf("python package %s not found", pkgName) 67 | } 68 | 69 | // do not check version if pkgVersion is empty 70 | if pkgVersion == "" { 71 | logger.Info("python package is ready", "name", pkgName) 72 | return nil 73 | } 74 | 75 | // check package version equality 76 | version := strings.TrimSpace(string(out)) 77 | if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") { 78 | return fmt.Errorf("python package %s version %s not matched, please upgrade to %s", 79 | pkgName, version, pkgVersion) 80 | } 81 | 82 | logger.Info("python package is ready", "name", pkgName, "version", pkgVersion) 83 | return nil 84 | } 85 | 86 | func InstallPythonPackage(python3 string, pkg string) (err error) { 87 | var pkgName, pkgVersion string 88 | if strings.Contains(pkg, "==") { 89 | // specify package version 90 | // funppy==0.5.0 91 | pkgInfo := strings.Split(pkg, "==") 92 | pkgName = pkgInfo[0] 93 | pkgVersion = pkgInfo[1] 94 | } else { 95 | // package version not specified, install the latest by default 96 | // funppy 97 | pkgName = pkg 98 | } 99 | 100 | // check if package installed and version matched 101 | err = AssertPythonPackage(python3, pkgName, pkgVersion) 102 | if err == nil { 103 | return nil 104 | } 105 | 106 | // check if pip available 107 | err = RunCommand(python3, "-m", "pip", "--version") 108 | if err != nil { 109 | logger.Warn("pip is not available") 110 | return errors.Wrap(err, "pip is not available") 111 | } 112 | 113 | logger.Info("installing python package", "pkgName", 114 | pkgName, "pkgVersion", pkgVersion) 115 | 116 | // install package 117 | pypiIndexURL := PYPI_INDEX_URL 118 | if pypiIndexURL == "" { 119 | pypiIndexURL = "https://pypi.org/simple" // default 120 | } 121 | err = RunCommand(python3, "-m", "pip", "install", pkg, "--upgrade", 122 | "--index-url", pypiIndexURL, 123 | "--quiet", "--disable-pip-version-check") 124 | if err != nil { 125 | return errors.Wrap(err, "pip install package failed") 126 | } 127 | 128 | return AssertPythonPackage(python3, pkgName, pkgVersion) 129 | } 130 | 131 | func RunShell(shellString string) (exitCode int, err error) { 132 | cmd := initShellExec(shellString) 133 | logger.Info("exec shell string", "content", cmd.String()) 134 | 135 | cmd.Stdout = os.Stdout 136 | cmd.Stderr = os.Stderr 137 | 138 | err = cmd.Start() 139 | if err != nil { 140 | return 1, errors.Wrap(err, "start running command failed") 141 | } 142 | 143 | // wait command done and get exit code 144 | err = cmd.Wait() 145 | if err != nil { 146 | exitErr, ok := err.(*exec.ExitError) 147 | if !ok { 148 | return 1, errors.Wrap(err, "get command exit code failed") 149 | } 150 | 151 | // got failed command exit code 152 | exitCode := exitErr.ExitCode() 153 | logger.Error("exec command failed", "exitCode", exitCode, "error", err) 154 | return exitCode, err 155 | } 156 | 157 | return 0, nil 158 | } 159 | 160 | func RunCommand(cmdName string, args ...string) error { 161 | cmd := Command(cmdName, args...) 162 | logger.Info("run command", "cmd", cmd.String()) 163 | 164 | // add cmd dir path to $PATH 165 | if cmdDir := filepath.Dir(cmdName); cmdDir != "" { 166 | var path string 167 | if runtime.GOOS == "windows" { 168 | path = fmt.Sprintf("%s;%s", cmdDir, PATH) 169 | } else { 170 | path = fmt.Sprintf("%s:%s", cmdDir, PATH) 171 | } 172 | if err := os.Setenv("PATH", path); err != nil { 173 | logger.Error("set env $PATH failed", "error", err) 174 | return err 175 | } 176 | } 177 | 178 | _, err := RunShell(cmd.String()) 179 | return err 180 | } 181 | 182 | func ExecCommandInDir(cmd *exec.Cmd, dir string) error { 183 | logger.Info("exec command", "cmd", cmd.String(), "dir", dir) 184 | cmd.Dir = dir 185 | 186 | // print stderr output 187 | var stderr bytes.Buffer 188 | cmd.Stderr = &stderr 189 | 190 | if err := cmd.Run(); err != nil { 191 | stderrStr := stderr.String() 192 | logger.Error("exec command failed", 193 | "error", err, "stderr", stderrStr) 194 | if stderrStr != "" { 195 | err = errors.Wrap(err, stderrStr) 196 | } 197 | return err 198 | } 199 | 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /fungo/utils_test.go: -------------------------------------------------------------------------------- 1 | package fungo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type data struct { 13 | f interface{} 14 | args []interface{} 15 | expVal interface{} 16 | expErr error 17 | } 18 | 19 | func TestCallFuncBasic(t *testing.T) { 20 | params := []data{ 21 | // zero argument, zero return 22 | {f: func() {}, args: []interface{}{}, expVal: nil, expErr: nil}, 23 | // zero argument, return one value 24 | {f: func() int { return 1 }, args: []interface{}{}, expVal: 1, expErr: nil}, 25 | {f: func() string { return "a" }, args: []interface{}{}, expVal: "a", expErr: nil}, 26 | {f: func() interface{} { return 1.23 }, args: []interface{}{}, expVal: 1.23, expErr: nil}, 27 | // zero argument, return error 28 | {f: func() error { return errors.New("xxx") }, args: []interface{}{}, expVal: nil, expErr: errors.New("xxx")}, 29 | // zero argument, return one value and error 30 | {f: func() (int, error) { return 1, errors.New("xxx") }, args: []interface{}{}, expVal: 1, expErr: errors.New("xxx")}, 31 | {f: func() (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{}, expVal: 1.23, expErr: errors.New("xxx")}, 32 | 33 | // one argument, zero return 34 | {f: func(n int) {}, args: []interface{}{1}, expVal: nil, expErr: nil}, 35 | // one argument, return one value 36 | {f: func(n int) int { return n * n }, args: []interface{}{2}, expVal: 4}, 37 | {f: func(c string) string { return c + c }, args: []interface{}{"p"}, expVal: "pp"}, 38 | {f: func(arg interface{}) interface{} { return fmt.Sprintf("%v", arg) }, args: []interface{}{1.23}, expVal: "1.23"}, 39 | // one argument, return one value and error 40 | {f: func(arg interface{}) (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{"a"}, expVal: 1.23, expErr: errors.New("xxx")}, 41 | 42 | // two arguments in same type 43 | {f: func(a, b int) int { return a * b }, args: []interface{}{2, 3}, expVal: 6}, 44 | // two arguments in different type 45 | { 46 | f: func(n int, c string) string { 47 | var s string 48 | for i := 0; i < n; i++ { 49 | s += c 50 | } 51 | return s 52 | }, 53 | args: []interface{}{3, "p"}, 54 | expVal: "ppp", 55 | }, 56 | 57 | // variable arguments list: ...int, ...interface{} 58 | { 59 | f: func(n ...int) int { 60 | var sum int 61 | for _, arg := range n { 62 | sum += arg 63 | } 64 | return sum 65 | }, 66 | args: []interface{}{1, 2, 3}, 67 | expVal: 6, 68 | }, 69 | { 70 | f: func(args ...interface{}) (interface{}, error) { 71 | var result string 72 | for _, arg := range args { 73 | result += fmt.Sprintf("%v", arg) 74 | } 75 | return result, nil 76 | }, 77 | args: []interface{}{1, 2.3, "4.5", "p"}, 78 | expVal: "12.34.5p", 79 | }, 80 | { 81 | f: func(a, b int8, n ...int) int { 82 | var sum int 83 | for _, arg := range n { 84 | sum += arg 85 | } 86 | sum += int(a) + int(b) 87 | return sum 88 | }, 89 | args: []interface{}{1, 2, 3, 4.5}, 90 | expVal: 10, 91 | }, 92 | { 93 | f: func(a, b int8, n ...int) int { 94 | sum := int(a) + int(b) 95 | for _, arg := range n { 96 | sum += arg 97 | } 98 | return sum 99 | }, 100 | args: []interface{}{1, 2}, 101 | expVal: 3, 102 | }, 103 | 104 | { 105 | f: func(a []int, n ...int) int { 106 | var sum int 107 | for _, arg := range a { 108 | sum += arg 109 | } 110 | for _, arg := range n { 111 | sum += arg 112 | } 113 | return sum 114 | }, 115 | args: []interface{}{[]int{1, 2}, 3, 4}, 116 | expVal: 10, 117 | }, 118 | } 119 | 120 | for _, p := range params { 121 | fn := reflect.ValueOf(p.f) 122 | val, err := CallFunc(fn, p.args...) 123 | if !assert.Equal(t, p.expErr, err) { 124 | t.Fatal(err) 125 | } 126 | if !assert.Equal(t, p.expVal, val) { 127 | t.Fatal() 128 | } 129 | } 130 | 131 | } 132 | 133 | func TestCallFuncComplex(t *testing.T) { 134 | params := []data{ 135 | // arguments include slice 136 | { 137 | f: func(a int, n []int, b int) int { 138 | sum := a 139 | for _, arg := range n { 140 | sum += arg 141 | } 142 | sum += b 143 | return sum 144 | }, 145 | args: []interface{}{1, []int{2, 3}, 4}, 146 | expVal: 10, 147 | }, 148 | // last argument is slice 149 | { 150 | f: func(n []int) int { 151 | var sum int 152 | for _, arg := range n { 153 | sum += arg 154 | } 155 | return sum 156 | }, 157 | args: []interface{}{[]int{1, 2, 3}}, 158 | expVal: 6, 159 | }, 160 | { 161 | f: func(a, b int, n []int) int { 162 | sum := a + b 163 | for _, arg := range n { 164 | sum += arg 165 | } 166 | return sum 167 | }, 168 | args: []interface{}{1, 2, []int{3, 4}}, 169 | expVal: 10, 170 | }, 171 | } 172 | 173 | for _, p := range params { 174 | fn := reflect.ValueOf(p.f) 175 | val, err := CallFunc(fn, p.args...) 176 | if !assert.Equal(t, p.expErr, err) { 177 | t.Fatal(err) 178 | } 179 | if !assert.Equal(t, p.expVal, val) { 180 | t.Fatal() 181 | } 182 | } 183 | 184 | } 185 | 186 | func TestCallFuncAbnormal(t *testing.T) { 187 | params := []data{ 188 | // return more than 2 values 189 | { 190 | f: func() (int, int, error) { return 1, 2, nil }, 191 | args: []interface{}{}, 192 | expVal: nil, 193 | expErr: fmt.Errorf("function should return at most 2 values"), 194 | }, 195 | } 196 | 197 | for _, p := range params { 198 | fn := reflect.ValueOf(p.f) 199 | val, err := CallFunc(fn, p.args...) 200 | if !assert.Equal(t, p.expErr, err) { 201 | t.Fatal(err) 202 | } 203 | if !assert.Equal(t, p.expVal, val) { 204 | t.Fatal() 205 | } 206 | } 207 | 208 | } 209 | 210 | func TestConvertCommonName(t *testing.T) { 211 | testData := []struct { 212 | expectedValue string 213 | originalValue string 214 | }{ 215 | { 216 | expectedValue: "httprunner", 217 | originalValue: "HttpRunner", 218 | }, 219 | { 220 | expectedValue: "httprunner", 221 | originalValue: "http_runner", 222 | }, 223 | { 224 | expectedValue: "httprunner", 225 | originalValue: "_http_runner", 226 | }, 227 | { 228 | expectedValue: "httprunner", 229 | originalValue: "HTTP_Runner", 230 | }, 231 | } 232 | for _, data := range testData { 233 | name := ConvertCommonName(data.originalValue) 234 | if !assert.Equal(t, data.expectedValue, name) { 235 | t.Fatal() 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 7 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 8 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 9 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 10 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 11 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 14 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 15 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 16 | github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= 17 | github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= 18 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 19 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 20 | github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= 21 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 22 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 25 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 26 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 27 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 32 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 33 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 34 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 35 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 36 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 37 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 38 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 39 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 40 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 41 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 42 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 45 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 46 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 47 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 48 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 49 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 50 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 54 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 55 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 58 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 59 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 60 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 61 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 62 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 63 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 71 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 73 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e h1:S83+ibolgyZ0bqz7KEsUOPErxcv4VzlszxY+31OfB/E= 76 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= 77 | google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= 78 | google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= 79 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 80 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 81 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 82 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 86 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 87 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /fungo/protoGen/debugtalk.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.19.4 5 | // source: proto/debugtalk.proto 6 | 7 | package protoGen 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Empty struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | } 28 | 29 | func (x *Empty) Reset() { 30 | *x = Empty{} 31 | if protoimpl.UnsafeEnabled { 32 | mi := &file_proto_debugtalk_proto_msgTypes[0] 33 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 34 | ms.StoreMessageInfo(mi) 35 | } 36 | } 37 | 38 | func (x *Empty) String() string { 39 | return protoimpl.X.MessageStringOf(x) 40 | } 41 | 42 | func (*Empty) ProtoMessage() {} 43 | 44 | func (x *Empty) ProtoReflect() protoreflect.Message { 45 | mi := &file_proto_debugtalk_proto_msgTypes[0] 46 | if protoimpl.UnsafeEnabled && x != nil { 47 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 48 | if ms.LoadMessageInfo() == nil { 49 | ms.StoreMessageInfo(mi) 50 | } 51 | return ms 52 | } 53 | return mi.MessageOf(x) 54 | } 55 | 56 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 57 | func (*Empty) Descriptor() ([]byte, []int) { 58 | return file_proto_debugtalk_proto_rawDescGZIP(), []int{0} 59 | } 60 | 61 | type GetNamesResponse struct { 62 | state protoimpl.MessageState 63 | sizeCache protoimpl.SizeCache 64 | unknownFields protoimpl.UnknownFields 65 | 66 | Names []string `protobuf:"bytes,1,rep,name=names,proto3" json:"names,omitempty"` 67 | } 68 | 69 | func (x *GetNamesResponse) Reset() { 70 | *x = GetNamesResponse{} 71 | if protoimpl.UnsafeEnabled { 72 | mi := &file_proto_debugtalk_proto_msgTypes[1] 73 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 74 | ms.StoreMessageInfo(mi) 75 | } 76 | } 77 | 78 | func (x *GetNamesResponse) String() string { 79 | return protoimpl.X.MessageStringOf(x) 80 | } 81 | 82 | func (*GetNamesResponse) ProtoMessage() {} 83 | 84 | func (x *GetNamesResponse) ProtoReflect() protoreflect.Message { 85 | mi := &file_proto_debugtalk_proto_msgTypes[1] 86 | if protoimpl.UnsafeEnabled && x != nil { 87 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 88 | if ms.LoadMessageInfo() == nil { 89 | ms.StoreMessageInfo(mi) 90 | } 91 | return ms 92 | } 93 | return mi.MessageOf(x) 94 | } 95 | 96 | // Deprecated: Use GetNamesResponse.ProtoReflect.Descriptor instead. 97 | func (*GetNamesResponse) Descriptor() ([]byte, []int) { 98 | return file_proto_debugtalk_proto_rawDescGZIP(), []int{1} 99 | } 100 | 101 | func (x *GetNamesResponse) GetNames() []string { 102 | if x != nil { 103 | return x.Names 104 | } 105 | return nil 106 | } 107 | 108 | type CallRequest struct { 109 | state protoimpl.MessageState 110 | sizeCache protoimpl.SizeCache 111 | unknownFields protoimpl.UnknownFields 112 | 113 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 114 | Args []byte `protobuf:"bytes,2,opt,name=args,proto3" json:"args,omitempty"` // []interface{} 115 | } 116 | 117 | func (x *CallRequest) Reset() { 118 | *x = CallRequest{} 119 | if protoimpl.UnsafeEnabled { 120 | mi := &file_proto_debugtalk_proto_msgTypes[2] 121 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 122 | ms.StoreMessageInfo(mi) 123 | } 124 | } 125 | 126 | func (x *CallRequest) String() string { 127 | return protoimpl.X.MessageStringOf(x) 128 | } 129 | 130 | func (*CallRequest) ProtoMessage() {} 131 | 132 | func (x *CallRequest) ProtoReflect() protoreflect.Message { 133 | mi := &file_proto_debugtalk_proto_msgTypes[2] 134 | if protoimpl.UnsafeEnabled && x != nil { 135 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 136 | if ms.LoadMessageInfo() == nil { 137 | ms.StoreMessageInfo(mi) 138 | } 139 | return ms 140 | } 141 | return mi.MessageOf(x) 142 | } 143 | 144 | // Deprecated: Use CallRequest.ProtoReflect.Descriptor instead. 145 | func (*CallRequest) Descriptor() ([]byte, []int) { 146 | return file_proto_debugtalk_proto_rawDescGZIP(), []int{2} 147 | } 148 | 149 | func (x *CallRequest) GetName() string { 150 | if x != nil { 151 | return x.Name 152 | } 153 | return "" 154 | } 155 | 156 | func (x *CallRequest) GetArgs() []byte { 157 | if x != nil { 158 | return x.Args 159 | } 160 | return nil 161 | } 162 | 163 | type CallResponse struct { 164 | state protoimpl.MessageState 165 | sizeCache protoimpl.SizeCache 166 | unknownFields protoimpl.UnknownFields 167 | 168 | Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // interface{} 169 | } 170 | 171 | func (x *CallResponse) Reset() { 172 | *x = CallResponse{} 173 | if protoimpl.UnsafeEnabled { 174 | mi := &file_proto_debugtalk_proto_msgTypes[3] 175 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 176 | ms.StoreMessageInfo(mi) 177 | } 178 | } 179 | 180 | func (x *CallResponse) String() string { 181 | return protoimpl.X.MessageStringOf(x) 182 | } 183 | 184 | func (*CallResponse) ProtoMessage() {} 185 | 186 | func (x *CallResponse) ProtoReflect() protoreflect.Message { 187 | mi := &file_proto_debugtalk_proto_msgTypes[3] 188 | if protoimpl.UnsafeEnabled && x != nil { 189 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 190 | if ms.LoadMessageInfo() == nil { 191 | ms.StoreMessageInfo(mi) 192 | } 193 | return ms 194 | } 195 | return mi.MessageOf(x) 196 | } 197 | 198 | // Deprecated: Use CallResponse.ProtoReflect.Descriptor instead. 199 | func (*CallResponse) Descriptor() ([]byte, []int) { 200 | return file_proto_debugtalk_proto_rawDescGZIP(), []int{3} 201 | } 202 | 203 | func (x *CallResponse) GetValue() []byte { 204 | if x != nil { 205 | return x.Value 206 | } 207 | return nil 208 | } 209 | 210 | var File_proto_debugtalk_proto protoreflect.FileDescriptor 211 | 212 | var file_proto_debugtalk_proto_rawDesc = []byte{ 213 | 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x74, 0x61, 0x6c, 214 | 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 215 | 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x28, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4e, 0x61, 216 | 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 217 | 0x61, 0x6d, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 218 | 0x73, 0x22, 0x35, 0x0a, 0x0b, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 219 | 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 220 | 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 221 | 0x28, 0x0c, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x22, 0x24, 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x6c, 222 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 223 | 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x6f, 224 | 0x0a, 0x09, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x61, 0x6c, 0x6b, 0x12, 0x31, 0x0a, 0x08, 0x47, 225 | 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 226 | 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 227 | 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 228 | 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 229 | 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 230 | 0x74, 0x6f, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 231 | 0x0d, 0x5a, 0x0b, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x47, 0x65, 0x6e, 0x62, 0x06, 232 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 233 | } 234 | 235 | var ( 236 | file_proto_debugtalk_proto_rawDescOnce sync.Once 237 | file_proto_debugtalk_proto_rawDescData = file_proto_debugtalk_proto_rawDesc 238 | ) 239 | 240 | func file_proto_debugtalk_proto_rawDescGZIP() []byte { 241 | file_proto_debugtalk_proto_rawDescOnce.Do(func() { 242 | file_proto_debugtalk_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_debugtalk_proto_rawDescData) 243 | }) 244 | return file_proto_debugtalk_proto_rawDescData 245 | } 246 | 247 | var file_proto_debugtalk_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 248 | var file_proto_debugtalk_proto_goTypes = []interface{}{ 249 | (*Empty)(nil), // 0: proto.Empty 250 | (*GetNamesResponse)(nil), // 1: proto.GetNamesResponse 251 | (*CallRequest)(nil), // 2: proto.CallRequest 252 | (*CallResponse)(nil), // 3: proto.CallResponse 253 | } 254 | var file_proto_debugtalk_proto_depIdxs = []int32{ 255 | 0, // 0: proto.DebugTalk.GetNames:input_type -> proto.Empty 256 | 2, // 1: proto.DebugTalk.Call:input_type -> proto.CallRequest 257 | 1, // 2: proto.DebugTalk.GetNames:output_type -> proto.GetNamesResponse 258 | 3, // 3: proto.DebugTalk.Call:output_type -> proto.CallResponse 259 | 2, // [2:4] is the sub-list for method output_type 260 | 0, // [0:2] is the sub-list for method input_type 261 | 0, // [0:0] is the sub-list for extension type_name 262 | 0, // [0:0] is the sub-list for extension extendee 263 | 0, // [0:0] is the sub-list for field type_name 264 | } 265 | 266 | func init() { file_proto_debugtalk_proto_init() } 267 | func file_proto_debugtalk_proto_init() { 268 | if File_proto_debugtalk_proto != nil { 269 | return 270 | } 271 | if !protoimpl.UnsafeEnabled { 272 | file_proto_debugtalk_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 273 | switch v := v.(*Empty); i { 274 | case 0: 275 | return &v.state 276 | case 1: 277 | return &v.sizeCache 278 | case 2: 279 | return &v.unknownFields 280 | default: 281 | return nil 282 | } 283 | } 284 | file_proto_debugtalk_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 285 | switch v := v.(*GetNamesResponse); i { 286 | case 0: 287 | return &v.state 288 | case 1: 289 | return &v.sizeCache 290 | case 2: 291 | return &v.unknownFields 292 | default: 293 | return nil 294 | } 295 | } 296 | file_proto_debugtalk_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 297 | switch v := v.(*CallRequest); i { 298 | case 0: 299 | return &v.state 300 | case 1: 301 | return &v.sizeCache 302 | case 2: 303 | return &v.unknownFields 304 | default: 305 | return nil 306 | } 307 | } 308 | file_proto_debugtalk_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 309 | switch v := v.(*CallResponse); i { 310 | case 0: 311 | return &v.state 312 | case 1: 313 | return &v.sizeCache 314 | case 2: 315 | return &v.unknownFields 316 | default: 317 | return nil 318 | } 319 | } 320 | } 321 | type x struct{} 322 | out := protoimpl.TypeBuilder{ 323 | File: protoimpl.DescBuilder{ 324 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 325 | RawDescriptor: file_proto_debugtalk_proto_rawDesc, 326 | NumEnums: 0, 327 | NumMessages: 4, 328 | NumExtensions: 0, 329 | NumServices: 1, 330 | }, 331 | GoTypes: file_proto_debugtalk_proto_goTypes, 332 | DependencyIndexes: file_proto_debugtalk_proto_depIdxs, 333 | MessageInfos: file_proto_debugtalk_proto_msgTypes, 334 | }.Build() 335 | File_proto_debugtalk_proto = out.File 336 | file_proto_debugtalk_proto_rawDesc = nil 337 | file_proto_debugtalk_proto_goTypes = nil 338 | file_proto_debugtalk_proto_depIdxs = nil 339 | } 340 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 debugtalk 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "colorama" 25 | version = "0.4.4" 26 | description = "Cross-platform colored terminal text." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [[package]] 32 | name = "grpcio" 33 | version = "1.44.0" 34 | description = "HTTP/2-based RPC framework" 35 | category = "main" 36 | optional = false 37 | python-versions = ">=3.6" 38 | 39 | [package.dependencies] 40 | six = ">=1.5.2" 41 | 42 | [package.extras] 43 | protobuf = ["grpcio-tools (>=1.44.0)"] 44 | 45 | [[package]] 46 | name = "grpcio-tools" 47 | version = "1.44.0" 48 | description = "Protobuf code generator for gRPC" 49 | category = "main" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | grpcio = ">=1.44.0" 55 | protobuf = ">=3.5.0.post1,<4.0dev" 56 | 57 | [[package]] 58 | name = "importlib-metadata" 59 | version = "4.8.3" 60 | description = "Read metadata from Python packages" 61 | category = "dev" 62 | optional = false 63 | python-versions = ">=3.6" 64 | 65 | [package.dependencies] 66 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 67 | zipp = ">=0.5" 68 | 69 | [package.extras] 70 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 71 | perf = ["ipython"] 72 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 73 | 74 | [[package]] 75 | name = "more-itertools" 76 | version = "8.12.0" 77 | description = "More routines for operating on iterables, beyond itertools" 78 | category = "dev" 79 | optional = false 80 | python-versions = ">=3.5" 81 | 82 | [[package]] 83 | name = "packaging" 84 | version = "21.3" 85 | description = "Core utilities for Python packages" 86 | category = "dev" 87 | optional = false 88 | python-versions = ">=3.6" 89 | 90 | [package.dependencies] 91 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 92 | 93 | [[package]] 94 | name = "pluggy" 95 | version = "0.13.1" 96 | description = "plugin and hook calling mechanisms for python" 97 | category = "dev" 98 | optional = false 99 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 100 | 101 | [package.dependencies] 102 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 103 | 104 | [package.extras] 105 | dev = ["pre-commit", "tox"] 106 | 107 | [[package]] 108 | name = "protobuf" 109 | version = "3.19.4" 110 | description = "Protocol Buffers" 111 | category = "main" 112 | optional = false 113 | python-versions = ">=3.5" 114 | 115 | [[package]] 116 | name = "py" 117 | version = "1.11.0" 118 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 122 | 123 | [[package]] 124 | name = "pyparsing" 125 | version = "3.0.7" 126 | description = "Python parsing module" 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=3.6" 130 | 131 | [package.extras] 132 | diagrams = ["jinja2", "railroad-diagrams"] 133 | 134 | [[package]] 135 | name = "pytest" 136 | version = "5.4.3" 137 | description = "pytest: simple powerful testing with Python" 138 | category = "dev" 139 | optional = false 140 | python-versions = ">=3.5" 141 | 142 | [package.dependencies] 143 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 144 | attrs = ">=17.4.0" 145 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 146 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 147 | more-itertools = ">=4.0.0" 148 | packaging = "*" 149 | pluggy = ">=0.12,<1.0" 150 | py = ">=1.5.0" 151 | wcwidth = "*" 152 | 153 | [package.extras] 154 | checkqa-mypy = ["mypy (==v0.761)"] 155 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 156 | 157 | [[package]] 158 | name = "six" 159 | version = "1.16.0" 160 | description = "Python 2 and 3 compatibility utilities" 161 | category = "main" 162 | optional = false 163 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 164 | 165 | [[package]] 166 | name = "typing-extensions" 167 | version = "4.1.1" 168 | description = "Backported and Experimental Type Hints for Python 3.6+" 169 | category = "dev" 170 | optional = false 171 | python-versions = ">=3.6" 172 | 173 | [[package]] 174 | name = "wcwidth" 175 | version = "0.2.5" 176 | description = "Measures the displayed width of unicode strings in a terminal" 177 | category = "dev" 178 | optional = false 179 | python-versions = "*" 180 | 181 | [[package]] 182 | name = "zipp" 183 | version = "3.6.0" 184 | description = "Backport of pathlib-compatible object wrapper for zip files" 185 | category = "dev" 186 | optional = false 187 | python-versions = ">=3.6" 188 | 189 | [package.extras] 190 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 191 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 192 | 193 | [metadata] 194 | lock-version = "1.1" 195 | python-versions = "^3.6" 196 | content-hash = "40981e6bd905b5a1fdb520ff7ad88cbbb382dbfb00daa310759bee4946de8aec" 197 | 198 | [metadata.files] 199 | atomicwrites = [ 200 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 201 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 202 | ] 203 | attrs = [ 204 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 205 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 206 | ] 207 | colorama = [ 208 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 209 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 210 | ] 211 | grpcio = [ 212 | {file = "grpcio-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:11f811c0fffd84fca747fbc742464575e5eb130fd4fb4d6012ccc34febd001db"}, 213 | {file = "grpcio-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9a86a91201f8345502ea81dee0a55ae13add5fafadf109b17acd858fe8239651"}, 214 | {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:5f3c54ebb5d9633a557335c01d88d3d4928e9b1b131692283b6184da1edbec0b"}, 215 | {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d47553b8e86ab1e59b0185ba6491a187f94a0239f414c8fc867a22b0405b798"}, 216 | {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1e22d3a510438b7f3365c0071b810672d09febac6e8ca8a47eab657ae5f347b"}, 217 | {file = "grpcio-1.44.0-cp310-cp310-win32.whl", hash = "sha256:41036a574cab3468f24d41d6ed2b52588fb85ed60f8feaa925d7e424a250740b"}, 218 | {file = "grpcio-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ee51964edfd0a1293a95bb0d72d134ecf889379d90d2612cbf663623ce832b4"}, 219 | {file = "grpcio-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:e2149077d71e060678130644670389ddf1491200bcea16c5560d4ccdc65e3f2e"}, 220 | {file = "grpcio-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:0ac72d4b953b76924f8fa21436af060d7e6d8581e279863f30ee14f20751ac27"}, 221 | {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c30a9a7d3a05920368a60b080cbbeaf06335303be23ac244034c71c03a0fd24"}, 222 | {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:05467acd391e3fffb05991c76cb2ed2fa1309d0e3815ac379764bc5670b4b5d4"}, 223 | {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b81dc7894062ed2d25b74a2725aaa0a6895ce97ce854f432fe4e87cad5a07316"}, 224 | {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46d4843192e7d36278884282e100b8f305cf37d1b3d8c6b4f736d4454640a069"}, 225 | {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898c159148f27e23c08a337fb80d31ece6b76bb24f359d83929460d813665b74"}, 226 | {file = "grpcio-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:b8d852329336c584c636caa9c2db990f3a332b19bc86a80f4646b58d27c142db"}, 227 | {file = "grpcio-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:790d7493337558ae168477d1be3178f4c9b8f91d8cd9b8b719d06fd9b2d48836"}, 228 | {file = "grpcio-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd61b52d9cf8fcf8d9628c0b640b9e44fdc5e93d989cc268086a858540ed370c"}, 229 | {file = "grpcio-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:14eefcf623890f3f7dd7831decd2a2116652b5ce1e0f1d4b464b8f52110743b0"}, 230 | {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:bebe90b8020b4248e5a2076b56154cc6ff45691bbbe980579fc9db26717ac968"}, 231 | {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89b390b1c0de909965280d175c53128ce2f0f4f5c0f011382243dd7f2f894060"}, 232 | {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:c122dac5cb299b8ad7308d61bd9fe0413de13b0347cce465398436b3fdf1f609"}, 233 | {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6641a28cc826a92ef717201cca9a035c34a0185e38b0c93f3ce5f01a01a1570a"}, 234 | {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb0a3e0e64843441793923d9532a3a23907b07b2a1e0a7a31f186dc185bb772"}, 235 | {file = "grpcio-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:be857b7ec2ac43455156e6ba89262f7d7ae60227049427d01a3fecd218a3f88d"}, 236 | {file = "grpcio-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f6a9cf0e77f72f2ac30c9c6e086bc7446c984c51bebc6c7f50fbcd718037edba"}, 237 | {file = "grpcio-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:19e54f0c7083c8332b5a75a9081fc5127f1dbb67b6c1a32bd7fe896ef0934918"}, 238 | {file = "grpcio-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:bfd36b959c3c4e945119387baed1414ea46f7116886aa23de0172302b49d7ff1"}, 239 | {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ccd388b8f37b19d06e4152189726ce309e36dc03b53f2216a4ea49f09a7438e6"}, 240 | {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9075c0c003c1ff14ebce8f0ba55cc692158cb55c68da09cf8b0f9fc5b749e343"}, 241 | {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e898194f76212facbaeb6d7545debff29351afa23b53ff8f0834d66611af5139"}, 242 | {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fa6584046a7cf281649975a363673fa5d9c6faf9dc923f261cc0e56713b5892"}, 243 | {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a7bdd6ef9bca050c7ade8cba5f0e743343ea0756d5d3d520e915098a9dc503"}, 244 | {file = "grpcio-1.44.0-cp38-cp38-win32.whl", hash = "sha256:dc3290d0411ddd2bd49adba5793223de8de8b01588d45e9376f1a9f7d25414f4"}, 245 | {file = "grpcio-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:13343e7b840c20f43b44f0e6d3bbdc037c964f0aec9735d7cb685c407731c9ff"}, 246 | {file = "grpcio-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c5c2f8417d13386e18ccc8c61467cb6a6f9667a1ff7000a2d7d378e5d7df693f"}, 247 | {file = "grpcio-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:cf220199b7b4992729ad4d55d5d3f652f4ccfe1a35b5eacdbecf189c245e1859"}, 248 | {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4201c597e5057a9bfef9ea5777a6d83f6252cb78044db7d57d941ec2300734a5"}, 249 | {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e2de61005118ae59d48d5d749283ebfd1ba4ca68cc1000f8a395cd2bdcff7ceb"}, 250 | {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:871078218fa9117e2a378678f327e32fda04e363ed6bc0477275444273255d4d"}, 251 | {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d610b7b557a7609fecee80b6dd793ecb7a9a3c3497fbdce63ce7d151cdd705"}, 252 | {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcb53e4eb8c271032c91b8981df5fc1bb974bc73e306ec2c27da41bd95c44b5"}, 253 | {file = "grpcio-1.44.0-cp39-cp39-win32.whl", hash = "sha256:e50ddea6de76c09b656df4b5a55ae222e2a56e625c44250e501ff3c904113ec1"}, 254 | {file = "grpcio-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:d2ec124a986093e26420a5fb10fa3f02b2c232f924cdd7b844ddf7e846c020cd"}, 255 | {file = "grpcio-1.44.0.tar.gz", hash = "sha256:4bae1c99896045d3062ab95478411c8d5a52cb84b91a1517312629fa6cfeb50e"}, 256 | ] 257 | grpcio-tools = [ 258 | {file = "grpcio-tools-1.44.0.tar.gz", hash = "sha256:be37f458ea510c9a8f1caabbc2b258d12e55d189a567f5edcace90f27dc0efbf"}, 259 | {file = "grpcio_tools-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:9f58529e24f613019a85c258a274d441d89e0cad8cf7fca21ef3807ba5840c5d"}, 260 | {file = "grpcio_tools-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:1d120082236f8d2877f8a19366476b82c3562423b877b7c471a142432e31c2c4"}, 261 | {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:65c2fe3cdc5425180f01dd303e28d4f363d38f4c2e3a7e1a87caedd5417e23bb"}, 262 | {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5caef118deb8cdee1978fd3d8e388a9b256cd8d34e4a8895731ac0e86fa5e47c"}, 263 | {file = "grpcio_tools-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121c9765cee8636201cf0d4e80bc7b509813194919bccdb66e9671c4ece6dac3"}, 264 | {file = "grpcio_tools-1.44.0-cp310-cp310-win32.whl", hash = "sha256:90d1fac188bac838c4169eb3b67197887fa0572ea8a90519a20cddb080800549"}, 265 | {file = "grpcio_tools-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e16260dfe6e997330473863e01466b0992369ae2337a0249b390b4651cff424"}, 266 | {file = "grpcio_tools-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:608414cc1093e1e9e5980c97a6ee78e51dffff359e7a3f123d1fb9d95b8763a5"}, 267 | {file = "grpcio_tools-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:395609c06f69fbc79518b30a01931127088a3f9ef2cc2a35269c5f187eefd38c"}, 268 | {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f7ce16766b24b88ec0e4355f5dd66c2eee6af210e889fcb7961c9c4634c687de"}, 269 | {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3c9abc4a40c62f46d5e43e49c7afc567dedf12eeef95933ac9ea2986baa2420b"}, 270 | {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b73fd87a44ba1b91866b0254193c37cdb001737759b77b637cebe0c816d38342"}, 271 | {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b211f12e4cbc0fde8e0f982b0f581cce38874666a02ebfed93c23dcaeb8a4e0"}, 272 | {file = "grpcio_tools-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b421dc9b27bcaff4c73644cd3801e4893b11ba3eb39729246fd3de98d9f685b"}, 273 | {file = "grpcio_tools-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:33d93027840a873c7b59402fe6db8263b88c56e2f84aa0b6281c05cc8bd314a1"}, 274 | {file = "grpcio_tools-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:71fb6e7e66b918803b1bebd0231560981ab86c2546a3318a45822ce94de5e83d"}, 275 | {file = "grpcio_tools-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:614c427ff235d92f103e9189f0230197c8f2f817d0dd9fd078f5d2ea4d920d02"}, 276 | {file = "grpcio_tools-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:c13e0cb486cfa15320ddcd70452a4d736e6ce319c03d6b3c0c2513ec8d2748fb"}, 277 | {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5ade6b13dc4e148f400c8f55a6ef0b14216a3371d7a9e559571d5981b6cec36b"}, 278 | {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6138d2c7eec7ed57585bc58e2dbcb65635a2d574ac632abd29949d3e68936bab"}, 279 | {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:3d6c8548b199591757dbfe89ed14e23782d6079d6d201c6c314c72f4086883aa"}, 280 | {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41c419829f01734d65958ba9b01b759061d8f7e0698f9612ba6b8837269f7a9"}, 281 | {file = "grpcio_tools-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9f0c5b4567631fec993826e694e83d86a972b3e2e9b05cb0c56839b0316d26c"}, 282 | {file = "grpcio_tools-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:3f0e1d1f3f5a6f0c9f8b5441819dbec831ce7e9ffe04768e4b0d965a95fbbe5e"}, 283 | {file = "grpcio_tools-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f87fc86d0b4181b6b4da6ec6a29511dca000e6b5694fdd6bbf87d125128bc41"}, 284 | {file = "grpcio_tools-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:cb8baa1d4cea35ca662c24098377bdd9514c56f227da0e38b43cd9b8223bfcc6"}, 285 | {file = "grpcio_tools-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:ea36a294f7c70fd2f2bfb5dcf08602006304aa65b055ebd4f7c709e2a89deba7"}, 286 | {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1972caf8f695b91edc6444134445798692fe71276f0cde7604d55e65179adf93"}, 287 | {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:674fb8d9c0e2d75166c4385753962485b757897223fc92a19c9e513ab80b96f7"}, 288 | {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:37045ba850d423cdacede77b266b127025818a5a36d80f1fd7a5a1614a6a0de5"}, 289 | {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cdf72947c6b0b03aa6dac06117a095947d02d43a5c6343051f4ce161fd0abcb"}, 290 | {file = "grpcio_tools-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bfa6fc1515c202fe428ba9f99e2b2f947b01bafc15d868798235b2e2d36baa"}, 291 | {file = "grpcio_tools-1.44.0-cp38-cp38-win32.whl", hash = "sha256:2c516124356476d9afa126acce10ce568733120afbd9ae17ee01d44b9da20a67"}, 292 | {file = "grpcio_tools-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6441c24176705c5ab056e65a8b330e107107c5a492ba094d1b862a136d15d"}, 293 | {file = "grpcio_tools-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:398eda759194d355eb09f7beabae6e4fb45b3877cf7efe505b49095fa4889cef"}, 294 | {file = "grpcio_tools-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:a169bfd7a1fe8cc11472eeeeab3088b3c5d56caac12b2192a920b73adcbc974c"}, 295 | {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:a58aaaec0d846d142edd8e794ebb80aa429abfd581f4493a60a603aac0c50ac8"}, 296 | {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c3253bee8b68fe422754faf0f286aa068861c926a7b11e4daeb44b9af767c7f1"}, 297 | {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3c0be60721ae1ba09c4f29572a145f412e561b9201e19428758893709827f472"}, 298 | {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e44b9572c2226b85976e0d6054e22d7c59ebd6c9425ee71e5bc8910434aee3e1"}, 299 | {file = "grpcio_tools-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c04ec47905c4f6d6dad34d29f6ace652cc1ddc986f55aaa5559b72104c3f5cf"}, 300 | {file = "grpcio_tools-1.44.0-cp39-cp39-win32.whl", hash = "sha256:fb8c7b9d24e2c4dc77e7800e83b68081729ac6094b781b2afdabf08af18c3b28"}, 301 | {file = "grpcio_tools-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:4eb93619c8cb3773fb899504e3e30a0dc79d3904fd7a84091d15552178e1e920"}, 302 | ] 303 | importlib-metadata = [ 304 | {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, 305 | {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, 306 | ] 307 | more-itertools = [ 308 | {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, 309 | {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, 310 | ] 311 | packaging = [ 312 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 313 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 314 | ] 315 | pluggy = [ 316 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 317 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 318 | ] 319 | protobuf = [ 320 | {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, 321 | {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, 322 | {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, 323 | {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, 324 | {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, 325 | {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, 326 | {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, 327 | {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, 328 | {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, 329 | {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, 330 | {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, 331 | {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, 332 | {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, 333 | {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, 334 | {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, 335 | {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, 336 | {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, 337 | {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, 338 | {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, 339 | {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, 340 | {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, 341 | {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, 342 | {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, 343 | {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, 344 | {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, 345 | {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, 346 | ] 347 | py = [ 348 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 349 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 350 | ] 351 | pyparsing = [ 352 | {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, 353 | {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, 354 | ] 355 | pytest = [ 356 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 357 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 358 | ] 359 | six = [ 360 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 361 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 362 | ] 363 | typing-extensions = [ 364 | {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, 365 | {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, 366 | ] 367 | wcwidth = [ 368 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 369 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 370 | ] 371 | zipp = [ 372 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 373 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 374 | ] 375 | --------------------------------------------------------------------------------