mirror of
				https://github.com/go-micro/go-micro.git
				synced 2025-10-30 23:27:41 +02:00 
			
		
		
		
	.
This commit is contained in:
		| @@ -30,11 +30,11 @@ const ( | ||||
| ) | ||||
|  | ||||
| type rpcClient struct { | ||||
| 	seq uint64 | ||||
| 	seq  uint64 | ||||
| 	opts Options | ||||
| 	once atomic.Value | ||||
| 	pool pool.Pool | ||||
| 	mu sync.RWMutex | ||||
| 	mu   sync.RWMutex | ||||
| } | ||||
|  | ||||
| func newRPCClient(opt ...Option) Client { | ||||
|   | ||||
							
								
								
									
										212
									
								
								cmd/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								cmd/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| # Micro | ||||
|  | ||||
| Go Micro Command Line | ||||
|  | ||||
| ## Install the CLI | ||||
|  | ||||
| Install `micro` via `go install` | ||||
|  | ||||
| ``` | ||||
| go install go-micro.dev/v5/cmd/micro@latest | ||||
| ``` | ||||
|  | ||||
| Or via install script | ||||
|  | ||||
|  | ||||
| ## Create a service | ||||
|  | ||||
| Create your service (all setup is now automatic!): | ||||
|  | ||||
| ``` | ||||
| micro new helloworld | ||||
| ``` | ||||
|  | ||||
| This will: | ||||
| - Create a new service in the `helloworld` directory | ||||
| - Automatically run `go mod tidy` and `make proto` for you | ||||
| - Show the updated project tree including generated files | ||||
| - Warn you if `protoc` is not installed, with install instructions | ||||
|  | ||||
| ## Run the service | ||||
|  | ||||
| Run the service | ||||
|  | ||||
| ``` | ||||
| micro run | ||||
| ``` | ||||
|  | ||||
| List services to see it's running and registered itself | ||||
|  | ||||
| ``` | ||||
| micro services | ||||
| ``` | ||||
|  | ||||
| ## Describe the service | ||||
|  | ||||
| Describe the service to see available endpoints | ||||
|  | ||||
| ``` | ||||
| micro describe helloworld | ||||
| ``` | ||||
|  | ||||
| Output | ||||
|  | ||||
| ``` | ||||
| { | ||||
|     "name": "helloworld", | ||||
|     "version": "latest", | ||||
|     "metadata": null, | ||||
|     "endpoints": [ | ||||
|         { | ||||
|             "request": { | ||||
|                 "name": "Request", | ||||
|                 "type": "Request", | ||||
|                 "values": [ | ||||
|                     { | ||||
|                         "name": "name", | ||||
|                         "type": "string", | ||||
|                         "values": null | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             "response": { | ||||
|                 "name": "Response", | ||||
|                 "type": "Response", | ||||
|                 "values": [ | ||||
|                     { | ||||
|                         "name": "msg", | ||||
|                         "type": "string", | ||||
|                         "values": null | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             "metadata": {}, | ||||
|             "name": "Helloworld.Call" | ||||
|         }, | ||||
|         { | ||||
|             "request": { | ||||
|                 "name": "Context", | ||||
|                 "type": "Context", | ||||
|                 "values": null | ||||
|             }, | ||||
|             "response": { | ||||
|                 "name": "Stream", | ||||
|                 "type": "Stream", | ||||
|                 "values": null | ||||
|             }, | ||||
|             "metadata": { | ||||
|                 "stream": "true" | ||||
|             }, | ||||
|             "name": "Helloworld.Stream" | ||||
|         } | ||||
|     ], | ||||
|     "nodes": [ | ||||
|         { | ||||
|             "metadata": { | ||||
|                 "broker": "http", | ||||
|                 "protocol": "mucp", | ||||
|                 "registry": "mdns", | ||||
|                 "server": "mucp", | ||||
|                 "transport": "http" | ||||
|             }, | ||||
|             "id": "helloworld-31e55be7-ac83-4810-89c8-a6192fb3ae83", | ||||
|             "address": "127.0.0.1:39963" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Call the service | ||||
|  | ||||
| Call via RPC endpoint | ||||
|  | ||||
| ``` | ||||
| micro call helloworld Helloworld.Call '{"name": "Asim"}' | ||||
| ``` | ||||
|  | ||||
| ## Create a client | ||||
|  | ||||
| Create a client to call the service | ||||
|  | ||||
| ```go | ||||
| package main | ||||
|  | ||||
| import ( | ||||
|         "context" | ||||
|         "fmt" | ||||
|  | ||||
|         "go-micro.dev/v5" | ||||
| ) | ||||
|  | ||||
| type Request struct { | ||||
|         Name string | ||||
| } | ||||
|  | ||||
| type Response struct { | ||||
|         Message string | ||||
| } | ||||
|  | ||||
| func main() { | ||||
|         client := micro.New("helloworld").Client() | ||||
|  | ||||
|         req := client.NewRequest("helloworld", "Helloworld.Call", &Request{Name: "John"}) | ||||
|  | ||||
|         var rsp Response | ||||
|  | ||||
|         err := client.Call(context.TODO(), req, &rsp) | ||||
|         if err != nil { | ||||
|                 fmt.Println(err) | ||||
|                 return | ||||
|         } | ||||
|  | ||||
|         fmt.Println(rsp.Message) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Protobuf  | ||||
|  | ||||
| Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro) | ||||
|  | ||||
| ## Server | ||||
|  | ||||
| The micro server is an api and web dashboard that provide a fixed entrypoint for seeing and querying services. | ||||
|  | ||||
| Run it like so | ||||
|  | ||||
| ``` | ||||
| micro server | ||||
| ``` | ||||
|  | ||||
| Then browse to [localhost:8080](http://localhost:8080) | ||||
|  | ||||
| ### API Endpoints  | ||||
|  | ||||
| The API provides a fixed HTTP entrypoint for calling services | ||||
|  | ||||
| ``` | ||||
| curl http://localhost:8080/api/helloworld/Helloworld/Call -d '{"name": "John"}' | ||||
| ``` | ||||
| See /api for more details and documentation for each service | ||||
|  | ||||
| ### Web Dashboard  | ||||
|  | ||||
| The web dashboard provides a modern, secure UI for managing and exploring your Micro services. Major features include: | ||||
|  | ||||
| - **Dynamic Service & Endpoint Forms**: Browse all registered services and endpoints. For each endpoint, a dynamic form is generated for easy testing and exploration. | ||||
| - **API Documentation**: The `/api` page lists all available services and endpoints, with request/response schemas and a sidebar for quick navigation. A documentation banner explains authentication requirements. | ||||
| - **JWT Authentication**: All login and token management uses a custom JWT utility. Passwords are securely stored with bcrypt. All `/api/x` endpoints and authenticated pages require an `Authorization: Bearer <token>` header (or `micro_token` cookie as fallback). | ||||
| - **Token Management**: The `/auth/tokens` page allows you to generate, view (obfuscated), and copy JWT tokens. Tokens are stored and can be revoked. When a user is deleted, all their tokens are revoked immediately. | ||||
| - **User Management**: The `/auth/users` page allows you to create, list, and delete users. Passwords are never shown or stored in plaintext. | ||||
| - **Token Revocation**: JWT tokens are stored and checked for revocation on every request. Revoked or deleted tokens are immediately invalidated. | ||||
| - **Security**: All protected endpoints use consistent authentication logic. Unauthorized or revoked tokens receive a 401 error. All sensitive actions require authentication. | ||||
| - **Logs & Status**: View service logs and status (PID, uptime, etc) directly from the dashboard. | ||||
|  | ||||
| To get started, run: | ||||
|  | ||||
| ``` | ||||
| micro server | ||||
| ``` | ||||
|  | ||||
| Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`). | ||||
|  | ||||
| > **Note:** See the `/api` page for details on API authentication and how to generate tokens for use with the HTTP API | ||||
							
								
								
									
										14
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								cmd/cmd.go
									
									
									
									
									
								
							| @@ -4,12 +4,16 @@ package cmd | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"sort" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/auth" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	nbroker "go-micro.dev/v5/broker/nats" | ||||
| 	rabbit "go-micro.dev/v5/broker/rabbitmq" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| 	"go-micro.dev/v5/cache/redis" | ||||
| 	"go-micro.dev/v5/client" | ||||
| @@ -19,15 +23,11 @@ import ( | ||||
| 	"go-micro.dev/v5/debug/profile/pprof" | ||||
| 	"go-micro.dev/v5/debug/trace" | ||||
| 	"go-micro.dev/v5/events" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	mprofile "go-micro.dev/v5/profile" | ||||
| 	"go-micro.dev/v5/auth" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	nbroker "go-micro.dev/v5/broker/nats" | ||||
| 	rabbit "go-micro.dev/v5/broker/rabbitmq" | ||||
| 	"go-micro.dev/v5/genai" | ||||
| 	"go-micro.dev/v5/genai/gemini" | ||||
| 	"go-micro.dev/v5/genai/openai" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	mprofile "go-micro.dev/v5/profile" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go-micro.dev/v5/registry/consul" | ||||
| 	"go-micro.dev/v5/registry/etcd" | ||||
|   | ||||
							
								
								
									
										223
									
								
								cmd/micro/cli/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								cmd/micro/cli/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| # Micro [](https://opensource.org/licenses/Apache-2.0) | ||||
|  | ||||
| A Go microservices toolkit | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Micro is a toolkit for Go microservices development. It provides the foundation for building services in the cloud.  | ||||
| The core of Micro is the [Go Micro](https://github.com/micro/go-micro) framework, which developers import and use in their code to  | ||||
| write services. Surrounding this we introduce a number of tools to make it easy to serve and consume services.  | ||||
|  | ||||
| ## Install the CLI | ||||
|  | ||||
| Install `micro` via `go install` | ||||
|  | ||||
| ``` | ||||
| go install go-micro.dev/v5@latest | ||||
| ``` | ||||
|  | ||||
| Or via install script | ||||
|  | ||||
| ``` | ||||
| wget -q  https://raw.githubusercontent.com/micro/micro/master/scripts/install.sh -O - | /bin/bash | ||||
| ``` | ||||
|  | ||||
| For releases see the [latest](https://go-micro.dev/releases/latest) tag | ||||
|  | ||||
| ## Create a service | ||||
|  | ||||
| Create your service (all setup is now automatic!): | ||||
|  | ||||
| ``` | ||||
| micro new helloworld | ||||
| ``` | ||||
|  | ||||
| This will: | ||||
| - Create a new service in the `helloworld` directory | ||||
| - Automatically run `go mod tidy` and `make proto` for you | ||||
| - Show the updated project tree including generated files | ||||
| - Warn you if `protoc` is not installed, with install instructions | ||||
|  | ||||
| ## Run the service | ||||
|  | ||||
| Run the service | ||||
|  | ||||
| ``` | ||||
| micro run | ||||
| ``` | ||||
|  | ||||
| List services to see it's running and registered itself | ||||
|  | ||||
| ``` | ||||
| micro services | ||||
| ``` | ||||
|  | ||||
| ## Describe the service | ||||
|  | ||||
| Describe the service to see available endpoints | ||||
|  | ||||
| ``` | ||||
| micro describe helloworld | ||||
| ``` | ||||
|  | ||||
| Output | ||||
|  | ||||
| ``` | ||||
| { | ||||
|     "name": "helloworld", | ||||
|     "version": "latest", | ||||
|     "metadata": null, | ||||
|     "endpoints": [ | ||||
|         { | ||||
|             "request": { | ||||
|                 "name": "Request", | ||||
|                 "type": "Request", | ||||
|                 "values": [ | ||||
|                     { | ||||
|                         "name": "name", | ||||
|                         "type": "string", | ||||
|                         "values": null | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             "response": { | ||||
|                 "name": "Response", | ||||
|                 "type": "Response", | ||||
|                 "values": [ | ||||
|                     { | ||||
|                         "name": "msg", | ||||
|                         "type": "string", | ||||
|                         "values": null | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             "metadata": {}, | ||||
|             "name": "Helloworld.Call" | ||||
|         }, | ||||
|         { | ||||
|             "request": { | ||||
|                 "name": "Context", | ||||
|                 "type": "Context", | ||||
|                 "values": null | ||||
|             }, | ||||
|             "response": { | ||||
|                 "name": "Stream", | ||||
|                 "type": "Stream", | ||||
|                 "values": null | ||||
|             }, | ||||
|             "metadata": { | ||||
|                 "stream": "true" | ||||
|             }, | ||||
|             "name": "Helloworld.Stream" | ||||
|         } | ||||
|     ], | ||||
|     "nodes": [ | ||||
|         { | ||||
|             "metadata": { | ||||
|                 "broker": "http", | ||||
|                 "protocol": "mucp", | ||||
|                 "registry": "mdns", | ||||
|                 "server": "mucp", | ||||
|                 "transport": "http" | ||||
|             }, | ||||
|             "id": "helloworld-31e55be7-ac83-4810-89c8-a6192fb3ae83", | ||||
|             "address": "127.0.0.1:39963" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Call the service | ||||
|  | ||||
| Call via RPC endpoint | ||||
|  | ||||
| ``` | ||||
| micro call helloworld Helloworld.Call '{"name": "Asim"}' | ||||
| ``` | ||||
|  | ||||
| ## Create a client | ||||
|  | ||||
| Create a client to call the service | ||||
|  | ||||
| ```go | ||||
| package main | ||||
|  | ||||
| import ( | ||||
|         "context" | ||||
|         "fmt" | ||||
|  | ||||
|         "go-micro.dev/v5" | ||||
| ) | ||||
|  | ||||
| type Request struct { | ||||
|         Name string | ||||
| } | ||||
|  | ||||
| type Response struct { | ||||
|         Message string | ||||
| } | ||||
|  | ||||
| func main() { | ||||
|         client := micro.New("helloworld").Client() | ||||
|  | ||||
|         req := client.NewRequest("helloworld", "Helloworld.Call", &Request{Name: "John"}) | ||||
|  | ||||
|         var rsp Response | ||||
|  | ||||
|         err := client.Call(context.TODO(), req, &rsp) | ||||
|         if err != nil { | ||||
|                 fmt.Println(err) | ||||
|                 return | ||||
|         } | ||||
|  | ||||
|         fmt.Println(rsp.Message) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Protobuf  | ||||
|  | ||||
| Use protobuf for code generation with [protoc-gen-micro](https://go-micro.dev/tree/master/cmd/protoc-gen-micro) | ||||
|  | ||||
| ## Server | ||||
|  | ||||
| The micro server is an api and web dashboard that provide a fixed entrypoint for seeing and querying services. | ||||
|  | ||||
| Run it like so | ||||
|  | ||||
| ``` | ||||
| micro server | ||||
| ``` | ||||
|  | ||||
| Then browse to [localhost:8080](http://localhost:8080) | ||||
|  | ||||
| ### API Endpoints  | ||||
|  | ||||
| The API provides a fixed HTTP entrypoint for calling services | ||||
|  | ||||
| ``` | ||||
| curl http://localhost:8080/api/helloworld/Helloworld/Call -d '{"name": "John"}' | ||||
| ``` | ||||
| See /api for more details and documentation for each service | ||||
|  | ||||
| ### Web Dashboard  | ||||
|  | ||||
| The web dashboard provides a modern, secure UI for managing and exploring your Micro services. Major features include: | ||||
|  | ||||
| - **Dynamic Service & Endpoint Forms**: Browse all registered services and endpoints. For each endpoint, a dynamic form is generated for easy testing and exploration. | ||||
| - **API Documentation**: The `/api` page lists all available services and endpoints, with request/response schemas and a sidebar for quick navigation. A documentation banner explains authentication requirements. | ||||
| - **JWT Authentication**: All login and token management uses a custom JWT utility. Passwords are securely stored with bcrypt. All `/api/x` endpoints and authenticated pages require an `Authorization: Bearer <token>` header (or `micro_token` cookie as fallback). | ||||
| - **Token Management**: The `/auth/tokens` page allows you to generate, view (obfuscated), and copy JWT tokens. Tokens are stored and can be revoked. When a user is deleted, all their tokens are revoked immediately. | ||||
| - **User Management**: The `/auth/users` page allows you to create, list, and delete users. Passwords are never shown or stored in plaintext. | ||||
| - **Token Revocation**: JWT tokens are stored and checked for revocation on every request. Revoked or deleted tokens are immediately invalidated. | ||||
| - **Security**: All protected endpoints use consistent authentication logic. Unauthorized or revoked tokens receive a 401 error. All sensitive actions require authentication. | ||||
| - **Logs & Status**: View service logs and status (PID, uptime, etc) directly from the dashboard. | ||||
|  | ||||
| To get started, run: | ||||
|  | ||||
| ``` | ||||
| micro server | ||||
| ``` | ||||
|  | ||||
| Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`). | ||||
|  | ||||
| > **Note:** See the `/api` page for details on API authentication and how to generate tokens for use with the HTTP API | ||||
							
								
								
									
										369
									
								
								cmd/micro/cli/cli.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								cmd/micro/cli/cli.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,369 @@ | ||||
| package microcli | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/signal" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/client" | ||||
| 	"go-micro.dev/v5/cmd" | ||||
| 	"go-micro.dev/v5/codec/bytes" | ||||
| 	"go-micro.dev/v5/genai" | ||||
| 	"go-micro.dev/v5/registry" | ||||
|  | ||||
| 	"go-micro.dev/v5/cmd/micro/cli/new" | ||||
| 	"go-micro.dev/v5/cmd/micro/cli/util" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// version is set by the release action | ||||
| 	// this is the default for local builds | ||||
| 	version = "5.0.0-dev" | ||||
| ) | ||||
|  | ||||
| func genProtoHandler(c *cli.Context) error { | ||||
| 	cmd := exec.Command("find", ".", "-name", "*.proto", "-exec", "protoc", "--proto_path=.", "--micro_out=.", "--go_out=.", `{}`, `;`) | ||||
| 	cmd.Stdout = os.Stdout | ||||
| 	cmd.Stderr = os.Stderr | ||||
| 	return cmd.Run() | ||||
| } | ||||
|  | ||||
| func genTextHandler(c *cli.Context) error { | ||||
| 	prompt := c.String("prompt") | ||||
| 	if len(prompt) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	gen := genai.DefaultGenAI | ||||
| 	if gen.String() == "noop" { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	res, err := gen.Generate(prompt) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println(res.Text) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func lastNonEmptyLine(s string) string { | ||||
| 	lines := strings.Split(s, "\n") | ||||
| 	for i := len(lines) - 1; i >= 0; i-- { | ||||
| 		if strings.TrimSpace(lines[i]) != "" { | ||||
| 			return lines[i] | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func lastLogLine(path string) string { | ||||
| 	f, err := os.Open(path) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	defer f.Close() | ||||
| 	var last string | ||||
| 	scan := bufio.NewScanner(f) | ||||
| 	for scan.Scan() { | ||||
| 		if strings.TrimSpace(scan.Text()) != "" { | ||||
| 			last = scan.Text() | ||||
| 		} | ||||
| 	} | ||||
| 	return last | ||||
| } | ||||
|  | ||||
| func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) { | ||||
| 	ch := make(chan os.Signal, 1) | ||||
| 	signal.Notify(ch, os.Interrupt) | ||||
| 	go func() { | ||||
| 		<-ch | ||||
| 		for _, proc := range procs { | ||||
| 			if proc.Process != nil { | ||||
| 				_ = proc.Process.Kill() | ||||
| 			} | ||||
| 		} | ||||
| 		for _, pf := range pidFiles { | ||||
| 			_ = os.Remove(pf) | ||||
| 		} | ||||
| 		os.Exit(1) | ||||
| 	}() | ||||
| 	for i, proc := range procs { | ||||
| 		_ = proc.Wait() | ||||
| 		if proc.Process != nil { | ||||
| 			_ = os.Remove(pidFiles[i]) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	cmd.Register([]*cli.Command{ | ||||
| 		{ | ||||
| 			Name:   "new", | ||||
| 			Usage:  "Create a new service", | ||||
| 			Action: new.Run, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "gen", | ||||
| 			Usage: "Generate various things", | ||||
| 			Subcommands: []*cli.Command{ | ||||
| 				{ | ||||
| 					Name:   "text", | ||||
| 					Usage:  "Generate text via an LLM", | ||||
| 					Action: genTextHandler, | ||||
| 					Flags: []cli.Flag{ | ||||
| 						&cli.StringFlag{ | ||||
| 							Name:    "prompt", | ||||
| 							Aliases: []string{"p"}, | ||||
| 							Usage:   "The prompt to generate text from", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:   "proto", | ||||
| 					Usage:  "Generate proto requires protoc and protoc-gen-micro", | ||||
| 					Action: genProtoHandler, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "services", | ||||
| 			Usage: "List available services", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				services, err := registry.ListServices() | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				for _, service := range services { | ||||
| 					fmt.Println(service.Name) | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "call", | ||||
| 			Usage: "Call a service", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				args := ctx.Args() | ||||
|  | ||||
| 				if args.Len() < 2 { | ||||
| 					return fmt.Errorf("Usage: [service] [endpoint] [request]") | ||||
| 				} | ||||
|  | ||||
| 				service := args.Get(0) | ||||
| 				endpoint := args.Get(1) | ||||
| 				request := `{}` | ||||
|  | ||||
| 				if args.Len() == 3 { | ||||
| 					request = args.Get(2) | ||||
| 				} | ||||
|  | ||||
| 				req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)}) | ||||
| 				var rsp bytes.Frame | ||||
| 				err := client.Call(context.TODO(), req, &rsp) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
|  | ||||
| 				fmt.Print(string(rsp.Data)) | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "describe", | ||||
| 			Usage: "Describe a service", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				args := ctx.Args() | ||||
|  | ||||
| 				if args.Len() != 1 { | ||||
| 					return fmt.Errorf("Usage: [service]") | ||||
| 				} | ||||
|  | ||||
| 				service := args.Get(0) | ||||
| 				services, err := registry.GetService(service) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				if len(services) == 0 { | ||||
| 					return nil | ||||
| 				} | ||||
| 				b, _ := json.MarshalIndent(services[0], "", "    ") | ||||
| 				fmt.Println(string(b)) | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "status", | ||||
| 			Usage: "Check status of running services", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				homeDir, err := os.UserHomeDir() | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("failed to get home dir: %w", err) | ||||
| 				} | ||||
| 				runDir := filepath.Join(homeDir, "micro", "run") | ||||
| 				files, err := os.ReadDir(runDir) | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("failed to read run dir: %w", err) | ||||
| 				} | ||||
| 				fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY") | ||||
| 				for _, f := range files { | ||||
| 					if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") { | ||||
| 						continue | ||||
| 					} | ||||
| 					service := f.Name()[:len(f.Name())-4] | ||||
| 					pidFilePath := filepath.Join(runDir, f.Name()) | ||||
| 					pidFile, err := os.Open(pidFilePath) | ||||
| 					if err != nil { | ||||
| 						continue | ||||
| 					} | ||||
| 					var pid int | ||||
| 					var dir string | ||||
| 					scanner := bufio.NewScanner(pidFile) | ||||
| 					if scanner.Scan() { | ||||
| 						fmt.Sscanf(scanner.Text(), "%d", &pid) | ||||
| 					} | ||||
| 					if scanner.Scan() { | ||||
| 						dir = scanner.Text() | ||||
| 					} | ||||
| 					pidFile.Close() | ||||
| 					status := "stopped" | ||||
| 					if pid > 0 { | ||||
| 						proc, err := os.FindProcess(pid) | ||||
| 						if err == nil { | ||||
| 							if err := proc.Signal(syscall.Signal(0)); err == nil { | ||||
| 								status = "running" | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 					fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir) | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "stop", | ||||
| 			Usage: "Stop a running service", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				if ctx.Args().Len() != 1 { | ||||
| 					return fmt.Errorf("Usage: micro stop [service]") | ||||
| 				} | ||||
| 				service := ctx.Args().Get(0) | ||||
| 				homeDir, err := os.UserHomeDir() | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("failed to get home dir: %w", err) | ||||
| 				} | ||||
| 				runDir := filepath.Join(homeDir, "micro", "run") | ||||
| 				pidFilePath := filepath.Join(runDir, service+".pid") | ||||
| 				pidFile, err := os.Open(pidFilePath) | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("no pid file for service %s", service) | ||||
| 				} | ||||
| 				var pid int | ||||
| 				var dir string | ||||
| 				scanner := bufio.NewScanner(pidFile) | ||||
| 				if scanner.Scan() { | ||||
| 					fmt.Sscanf(scanner.Text(), "%d", &pid) | ||||
| 				} | ||||
| 				if scanner.Scan() { | ||||
| 					dir = scanner.Text() | ||||
| 				} | ||||
| 				pidFile.Close() | ||||
| 				if pid <= 0 { | ||||
| 					_ = os.Remove(pidFilePath) | ||||
| 					return fmt.Errorf("service %s is not running", service) | ||||
| 				} | ||||
| 				proc, err := os.FindProcess(pid) | ||||
| 				if err != nil { | ||||
| 					_ = os.Remove(pidFilePath) | ||||
| 					return fmt.Errorf("could not find process for %s", service) | ||||
| 				} | ||||
| 				if err := proc.Signal(syscall.SIGTERM); err != nil { | ||||
| 					_ = os.Remove(pidFilePath) | ||||
| 					return fmt.Errorf("failed to stop service %s: %v", service, err) | ||||
| 				} | ||||
| 				_ = os.Remove(pidFilePath) | ||||
| 				fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir) | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:  "logs", | ||||
| 			Usage: "Show logs for a service, or list available logs if no service is specified", | ||||
| 			Action: func(ctx *cli.Context) error { | ||||
| 				homeDir, err := os.UserHomeDir() | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("failed to get home dir: %w", err) | ||||
| 				} | ||||
| 				logsDir := filepath.Join(homeDir, "micro", "logs") | ||||
| 				if ctx.Args().Len() == 0 { | ||||
| 					// List available logs | ||||
| 					dirEntries, err := os.ReadDir(logsDir) | ||||
| 					if err != nil { | ||||
| 						return fmt.Errorf("could not list logs directory: %v", err) | ||||
| 					} | ||||
| 					fmt.Println("Available logs:") | ||||
| 					found := false | ||||
| 					for _, entry := range dirEntries { | ||||
| 						if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") { | ||||
| 							fmt.Println("  ", strings.TrimSuffix(entry.Name(), ".log")) | ||||
| 							found = true | ||||
| 						} | ||||
| 					} | ||||
| 					if !found { | ||||
| 						fmt.Println("  (no logs found)") | ||||
| 					} | ||||
| 					return nil | ||||
| 				} | ||||
| 				service := ctx.Args().Get(0) | ||||
| 				logFilePath := filepath.Join(logsDir, service+".log") | ||||
| 				f, err := os.Open(logFilePath) | ||||
| 				if err != nil { | ||||
| 					return fmt.Errorf("could not open log file for service %s: %v", service, err) | ||||
| 				} | ||||
| 				defer f.Close() | ||||
| 				scan := bufio.NewScanner(f) | ||||
| 				for scan.Scan() { | ||||
| 					fmt.Println(scan.Text()) | ||||
| 				} | ||||
| 				return scan.Err() | ||||
| 			}, | ||||
| 		}, | ||||
| 	}...) | ||||
|  | ||||
| 	cmd.App().Action = func(c *cli.Context) error { | ||||
| 		if c.Args().Len() == 0 { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		v, err := exec.LookPath("micro-" + c.Args().First()) | ||||
| 		if err == nil { | ||||
| 			ce := exec.Command(v, c.Args().Slice()[1:]...) | ||||
| 			ce.Stdout = os.Stdout | ||||
| 			ce.Stderr = os.Stderr | ||||
| 			return ce.Run() | ||||
| 		} | ||||
|  | ||||
| 		command := c.Args().Get(0) | ||||
| 		args := c.Args().Slice() | ||||
|  | ||||
| 		if srv, err := util.LookupService(command); err != nil { | ||||
| 			return util.CliError(err) | ||||
| 		} else if srv != nil && util.ShouldRenderHelp(args) { | ||||
| 			return cli.Exit(util.FormatServiceUsage(srv, c), 0) | ||||
| 		} else if srv != nil { | ||||
| 			err := util.CallService(srv, args) | ||||
| 			return util.CliError(err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										257
									
								
								cmd/micro/cli/new/new.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								cmd/micro/cli/new/new.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,257 @@ | ||||
| // Package new generates micro service templates | ||||
| package new | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"go/build" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"github.com/xlab/treeprint" | ||||
| 	tmpl "go-micro.dev/v5/cmd/micro/cli/new/template" | ||||
| ) | ||||
|  | ||||
| func protoComments(goDir, alias string) []string { | ||||
| 	return []string{ | ||||
| 		"\ndownload protoc zip packages (protoc-$VERSION-$PLATFORM.zip) and install:\n", | ||||
| 		"visit https://github.com/protocolbuffers/protobuf/releases", | ||||
| 		"\ncompile the proto file " + alias + ".proto:\n", | ||||
| 		"cd " + alias, | ||||
| 		"go mod tidy", | ||||
| 		"make proto\n", | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type config struct { | ||||
| 	// foo | ||||
| 	Alias string | ||||
| 	// github.com/micro/foo | ||||
| 	Dir string | ||||
| 	// $GOPATH/src/github.com/micro/foo | ||||
| 	GoDir string | ||||
| 	// $GOPATH | ||||
| 	GoPath string | ||||
| 	// UseGoPath | ||||
| 	UseGoPath bool | ||||
| 	// Files | ||||
| 	Files []file | ||||
| 	// Comments | ||||
| 	Comments []string | ||||
| } | ||||
|  | ||||
| type file struct { | ||||
| 	Path string | ||||
| 	Tmpl string | ||||
| } | ||||
|  | ||||
| func write(c config, file, tmpl string) error { | ||||
| 	fn := template.FuncMap{ | ||||
| 		"title": func(s string) string { | ||||
| 			return strings.ReplaceAll(strings.Title(s), "-", "") | ||||
| 		}, | ||||
| 		"dehyphen": func(s string) string { | ||||
| 			return strings.ReplaceAll(s, "-", "") | ||||
| 		}, | ||||
| 		"lower": func(s string) string { | ||||
| 			return strings.ToLower(s) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	f, err := os.Create(file) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	t, err := template.New("f").Funcs(fn).Parse(tmpl) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return t.Execute(f, c) | ||||
| } | ||||
|  | ||||
| func create(c config) error { | ||||
| 	// check if dir exists | ||||
| 	if _, err := os.Stat(c.Dir); !os.IsNotExist(err) { | ||||
| 		return fmt.Errorf("%s already exists", c.Dir) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Creating service %s\n\n", c.Alias) | ||||
|  | ||||
| 	t := treeprint.New() | ||||
|  | ||||
| 	// write the files | ||||
| 	for _, file := range c.Files { | ||||
| 		f := filepath.Join(c.Dir, file.Path) | ||||
| 		dir := filepath.Dir(f) | ||||
|  | ||||
| 		if _, err := os.Stat(dir); os.IsNotExist(err) { | ||||
| 			if err := os.MkdirAll(dir, 0755); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		addFileToTree(t, file.Path) | ||||
| 		if err := write(c, f, file.Tmpl); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// print tree | ||||
| 	fmt.Println(t.String()) | ||||
|  | ||||
| 	for _, comment := range c.Comments { | ||||
| 		fmt.Println(comment) | ||||
| 	} | ||||
|  | ||||
| 	// just wait | ||||
| 	<-time.After(time.Millisecond * 250) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func addFileToTree(root treeprint.Tree, file string) { | ||||
| 	split := strings.Split(file, "/") | ||||
| 	curr := root | ||||
| 	for i := 0; i < len(split)-1; i++ { | ||||
| 		n := curr.FindByValue(split[i]) | ||||
| 		if n != nil { | ||||
| 			curr = n | ||||
| 		} else { | ||||
| 			curr = curr.AddBranch(split[i]) | ||||
| 		} | ||||
| 	} | ||||
| 	if curr.FindByValue(split[len(split)-1]) == nil { | ||||
| 		curr.AddNode(split[len(split)-1]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Run(ctx *cli.Context) error { | ||||
| 	dir := ctx.Args().First() | ||||
| 	if len(dir) == 0 { | ||||
| 		fmt.Println("specify service name") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// check if the path is absolute, we don't want this | ||||
| 	// we want to a relative path so we can install in GOPATH | ||||
| 	if path.IsAbs(dir) { | ||||
| 		fmt.Println("require relative path as service will be installed in GOPATH") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Check for protoc | ||||
| 	if _, err := exec.LookPath("protoc"); err != nil { | ||||
| 		fmt.Println("WARNING: protoc is not installed or not in your PATH.") | ||||
| 		fmt.Println("Please install protoc from https://github.com/protocolbuffers/protobuf/releases") | ||||
| 		fmt.Println("After installing, re-run 'make proto' in your service directory if needed.") | ||||
| 	} | ||||
|  | ||||
| 	var goPath string | ||||
| 	var goDir string | ||||
|  | ||||
| 	goPath = build.Default.GOPATH | ||||
|  | ||||
| 	// don't know GOPATH, runaway.... | ||||
| 	if len(goPath) == 0 { | ||||
| 		fmt.Println("unknown GOPATH") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// attempt to split path if not windows | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		goPath = strings.Split(goPath, ";")[0] | ||||
| 	} else { | ||||
| 		goPath = strings.Split(goPath, ":")[0] | ||||
| 	} | ||||
| 	goDir = filepath.Join(goPath, "src", path.Clean(dir)) | ||||
|  | ||||
| 	c := config{ | ||||
| 		Alias:     dir, | ||||
| 		Comments:  nil, // Remove redundant protoComments | ||||
| 		Dir:       dir, | ||||
| 		GoDir:     goDir, | ||||
| 		GoPath:    goPath, | ||||
| 		UseGoPath: false, | ||||
| 		Files: []file{ | ||||
| 			{"main.go", tmpl.MainSRV}, | ||||
| 			{"handler/" + dir + ".go", tmpl.HandlerSRV}, | ||||
| 			{"proto/" + dir + ".proto", tmpl.ProtoSRV}, | ||||
| 			{"Makefile", tmpl.Makefile}, | ||||
| 			{"README.md", tmpl.Readme}, | ||||
| 			{".gitignore", tmpl.GitIgnore}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// set gomodule | ||||
| 	if os.Getenv("GO111MODULE") != "off" { | ||||
| 		c.Files = append(c.Files, file{"go.mod", tmpl.Module}) | ||||
| 	} | ||||
|  | ||||
| 	// create the files | ||||
| 	if err := create(c); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Run go mod tidy and make proto | ||||
| 	fmt.Println("\nRunning 'go mod tidy' and 'make proto'...") | ||||
| 	if err := runInDir(dir, "go mod tidy"); err != nil { | ||||
| 		fmt.Printf("Error running 'go mod tidy': %v\n", err) | ||||
| 	} | ||||
| 	if err := runInDir(dir, "make proto"); err != nil { | ||||
| 		fmt.Printf("Error running 'make proto': %v\n", err) | ||||
| 	} | ||||
|  | ||||
| 	// Print updated tree including generated files | ||||
| 	fmt.Println("\nProject structure after 'make proto':") | ||||
| 	printTree(dir) | ||||
|  | ||||
| 	fmt.Println("\nService created successfully! Start coding in your new service directory.") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func runInDir(dir, cmd string) error { | ||||
| 	parts := strings.Fields(cmd) | ||||
| 	c := exec.Command(parts[0], parts[1:]...) | ||||
| 	c.Dir = dir | ||||
| 	c.Stdout = os.Stdout | ||||
| 	c.Stderr = os.Stderr | ||||
| 	return c.Run() | ||||
| } | ||||
|  | ||||
| func printTree(dir string) { | ||||
| 	t := treeprint.New() | ||||
| 	walk := func(path string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		rel, _ := filepath.Rel(dir, path) | ||||
| 		if rel == "." { | ||||
| 			return nil | ||||
| 		} | ||||
| 		parts := strings.Split(rel, string(os.PathSeparator)) | ||||
| 		curr := t | ||||
| 		for i := 0; i < len(parts)-1; i++ { | ||||
| 			n := curr.FindByValue(parts[i]) | ||||
| 			if n != nil { | ||||
| 				curr = n | ||||
| 			} else { | ||||
| 				curr = curr.AddBranch(parts[i]) | ||||
| 			} | ||||
| 		} | ||||
| 		if !info.IsDir() { | ||||
| 			curr.AddNode(parts[len(parts)-1]) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	filepath.Walk(dir, walk) | ||||
| 	fmt.Println(t.String()) | ||||
| } | ||||
							
								
								
									
										66
									
								
								cmd/micro/cli/new/template/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								cmd/micro/cli/new/template/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	HandlerSRV = `package handler | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	log "go-micro.dev/v5/logger" | ||||
|  | ||||
| 	pb "{{.Dir}}/proto" | ||||
| ) | ||||
|  | ||||
| type {{title .Alias}} struct{} | ||||
|  | ||||
| // Return a new handler | ||||
| func New() *{{title .Alias}} { | ||||
| 	return &{{title .Alias}}{} | ||||
| } | ||||
|  | ||||
| // Call is a single request handler called via client.Call or the generated client code | ||||
| func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error { | ||||
| 	log.Info("Received {{title .Alias}}.Call request") | ||||
| 	rsp.Msg = "Hello " + req.Name | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Stream is a server side stream handler called via client.Stream or the generated client code | ||||
| func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error { | ||||
| 	log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count) | ||||
|  | ||||
| 	for i := 0; i < int(req.Count); i++ { | ||||
| 		log.Infof("Responding: %d", i) | ||||
| 		if err := stream.Send(&pb.StreamingResponse{ | ||||
| 			Count: int64(i), | ||||
| 		}); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| ` | ||||
|  | ||||
| 	SubscriberSRV = `package subscriber | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	log "go-micro.dev/v5/logger" | ||||
|  | ||||
| 	pb "{{.Dir}}/proto" | ||||
| ) | ||||
|  | ||||
| type {{title .Alias}} struct{} | ||||
|  | ||||
| func (e *{{title .Alias}}) Handle(ctx context.Context, msg *pb.Message) error { | ||||
| 	log.Info("Handler Received message: ", msg.Say) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func Handler(ctx context.Context, msg *pb.Message) error { | ||||
| 	log.Info("Function Received message: ", msg.Say) | ||||
| 	return nil | ||||
| } | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										7
									
								
								cmd/micro/cli/new/template/ignore.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								cmd/micro/cli/new/template/ignore.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	GitIgnore = ` | ||||
| {{.Alias}} | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										27
									
								
								cmd/micro/cli/new/template/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								cmd/micro/cli/new/template/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	MainSRV = `package main | ||||
|  | ||||
| import ( | ||||
| 	"{{.Dir}}/handler" | ||||
| 	pb "{{.Dir}}/proto" | ||||
|  | ||||
| 	"go-micro.dev/v5" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// Create service | ||||
| 	service := micro.New("{{lower .Alias}}") | ||||
|  | ||||
| 	// Initialize service | ||||
| 	service.Init() | ||||
|  | ||||
| 	// Register handler | ||||
| 	pb.Register{{title .Alias}}Handler(service.Server(), handler.New()) | ||||
|  | ||||
| 	// Run service | ||||
| 	service.Run() | ||||
| } | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										32
									
								
								cmd/micro/cli/new/template/makefile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								cmd/micro/cli/new/template/makefile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	Makefile = ` | ||||
| GOPATH:=$(shell go env GOPATH) | ||||
| .PHONY: init | ||||
| init: | ||||
| 	go install google.golang.org/protobuf/cmd/protoc-gen-go@latest | ||||
| 	go install go-micro.dev/v5/cmd/protoc-gen-micro@latest | ||||
| 	go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest | ||||
|  | ||||
| .PHONY: api | ||||
| api: | ||||
| 	protoc --openapi_out=. --proto_path=. proto/{{.Alias}}.proto | ||||
|  | ||||
| .PHONY: proto | ||||
| proto: | ||||
| 	protoc --proto_path=. --micro_out=. --go_out=:. proto/{{.Alias}}.proto | ||||
| 	 | ||||
| .PHONY: build | ||||
| build: | ||||
| 	go build -o {{.Alias}} *.go | ||||
|  | ||||
| .PHONY: test | ||||
| test: | ||||
| 	go test -v ./... -cover | ||||
|  | ||||
| .PHONY: docker | ||||
| docker: | ||||
| 	docker build . -t {{.Alias}}:latest | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										14
									
								
								cmd/micro/cli/new/template/module.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cmd/micro/cli/new/template/module.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	Module = `module {{.Dir}} | ||||
|  | ||||
| go 1.18 | ||||
|  | ||||
| require ( | ||||
| 	go-micro.dev/v5 latest | ||||
| 	github.com/golang/protobuf latest | ||||
| 	google.golang.org/protobuf latest | ||||
| ) | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										35
									
								
								cmd/micro/cli/new/template/proto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								cmd/micro/cli/new/template/proto.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	ProtoSRV = `syntax = "proto3"; | ||||
|  | ||||
| package {{dehyphen .Alias}}; | ||||
|  | ||||
| option go_package = "./proto;{{dehyphen .Alias}}"; | ||||
|  | ||||
| service {{title .Alias}} { | ||||
| 	rpc Call(Request) returns (Response) {} | ||||
| 	rpc Stream(StreamingRequest) returns (stream StreamingResponse) {} | ||||
| } | ||||
|  | ||||
| message Message { | ||||
| 	string say = 1; | ||||
| } | ||||
|  | ||||
| message Request { | ||||
| 	string name = 1; | ||||
| } | ||||
|  | ||||
| message Response { | ||||
| 	string msg = 1; | ||||
| } | ||||
|  | ||||
| message StreamingRequest { | ||||
| 	int64 count = 1; | ||||
| } | ||||
|  | ||||
| message StreamingResponse { | ||||
| 	int64 count = 1; | ||||
| } | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										30
									
								
								cmd/micro/cli/new/template/readme.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								cmd/micro/cli/new/template/readme.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| package template | ||||
|  | ||||
| var ( | ||||
| 	Readme = `# {{title .Alias}} Service | ||||
|  | ||||
| This is the {{title .Alias}} service | ||||
|  | ||||
| Generated with | ||||
|  | ||||
| ` + "```" + | ||||
| 		` | ||||
| micro new {{.Alias}} | ||||
| ` + "```" + ` | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Generate the proto code | ||||
|  | ||||
| ` + "```" + | ||||
| 		` | ||||
| make proto | ||||
| ` + "```" + ` | ||||
|  | ||||
| Run the service | ||||
|  | ||||
| ` + "```" + | ||||
| 		` | ||||
| micro run . | ||||
| ` + "```" | ||||
| ) | ||||
							
								
								
									
										416
									
								
								cmd/micro/cli/util/dynamic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								cmd/micro/cli/util/dynamic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,416 @@ | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/stretchr/objx" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/client" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| // LookupService queries the service for a service with the given alias. If | ||||
| // no services are found for a given alias, the registry will return nil and | ||||
| // the error will also be nil. An error is only returned if there was an issue | ||||
| // listing from the registry. | ||||
| func LookupService(name string) (*registry.Service, error) { | ||||
| 	// return a lookup in the default domain as a catch all | ||||
| 	return serviceWithName(name) | ||||
| } | ||||
|  | ||||
| // FormatServiceUsage returns a string containing the service usage. | ||||
| func FormatServiceUsage(srv *registry.Service, c *cli.Context) string { | ||||
| 	alias := c.Args().First() | ||||
| 	subcommand := c.Args().Get(1) | ||||
|  | ||||
| 	commands := make([]string, len(srv.Endpoints)) | ||||
| 	endpoints := make([]*registry.Endpoint, len(srv.Endpoints)) | ||||
| 	for i, e := range srv.Endpoints { | ||||
| 		// map "Helloworld.Call" to "helloworld.call" | ||||
| 		parts := strings.Split(e.Name, ".") | ||||
| 		for i, part := range parts { | ||||
| 			parts[i] = lowercaseInitial(part) | ||||
| 		} | ||||
| 		name := strings.Join(parts, ".") | ||||
|  | ||||
| 		// remove the prefix if it is the service name, e.g. rather than | ||||
| 		// "micro run helloworld helloworld call", it would be | ||||
| 		// "micro run helloworld call". | ||||
| 		name = strings.TrimPrefix(name, alias+".") | ||||
|  | ||||
| 		// instead of "micro run helloworld foo.bar", the command should | ||||
| 		// be "micro run helloworld foo bar". | ||||
| 		commands[i] = strings.Replace(name, ".", " ", 1) | ||||
| 		endpoints[i] = e | ||||
| 	} | ||||
|  | ||||
| 	result := "" | ||||
| 	if len(subcommand) > 0 && subcommand != "--help" { | ||||
| 		result += fmt.Sprintf("NAME:\n\tmicro %v %v\n\n", alias, subcommand) | ||||
| 		result += fmt.Sprintf("USAGE:\n\tmicro %v %v [flags]\n\n", alias, subcommand) | ||||
| 		result += fmt.Sprintf("FLAGS:\n") | ||||
|  | ||||
| 		for i, command := range commands { | ||||
| 			if command == subcommand { | ||||
| 				result += renderFlags(endpoints[i]) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// sort the command names alphabetically | ||||
| 		sort.Strings(commands) | ||||
|  | ||||
| 		result += fmt.Sprintf("NAME:\n\tmicro %v\n\n", alias) | ||||
| 		result += fmt.Sprintf("VERSION:\n\t%v\n\n", srv.Version) | ||||
| 		result += fmt.Sprintf("USAGE:\n\tmicro %v [command]\n\n", alias) | ||||
| 		result += fmt.Sprintf("COMMANDS:\n\t%v\n", strings.Join(commands, "\n\t")) | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func lowercaseInitial(str string) string { | ||||
| 	for i, v := range str { | ||||
| 		return string(unicode.ToLower(v)) + str[i+1:] | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func renderFlags(endpoint *registry.Endpoint) string { | ||||
| 	ret := "" | ||||
| 	for _, value := range endpoint.Request.Values { | ||||
| 		ret += renderValue([]string{}, value) + "\n" | ||||
| 	} | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| func renderValue(path []string, value *registry.Value) string { | ||||
| 	if len(value.Values) > 0 { | ||||
| 		renders := []string{} | ||||
| 		for _, v := range value.Values { | ||||
| 			renders = append(renders, renderValue(append(path, value.Name), v)) | ||||
| 		} | ||||
| 		return strings.Join(renders, "\n") | ||||
| 	} | ||||
| 	return fmt.Sprintf("\t--%v %v", strings.Join(append(path, value.Name), "_"), value.Type) | ||||
| } | ||||
|  | ||||
| // CallService will call a service using the arguments and flags provided | ||||
| // in the context. It will print the result or error to stdout. If there | ||||
| // was an error performing the call, it will be returned. | ||||
| func CallService(srv *registry.Service, args []string) error { | ||||
| 	// parse the flags and args | ||||
| 	args, flags, err := splitCmdArgs(args) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// construct the endpoint | ||||
| 	endpoint, err := constructEndpoint(args) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// ensure the endpoint exists on the service | ||||
| 	var ep *registry.Endpoint | ||||
| 	for _, e := range srv.Endpoints { | ||||
| 		if e.Name == endpoint { | ||||
| 			ep = e | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if ep == nil { | ||||
| 		return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name) | ||||
| 	} | ||||
|  | ||||
| 	// parse the flags | ||||
| 	body, err := FlagsToRequest(flags, ep.Request) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// create a context for the call based on the cli context | ||||
| 	callCtx := context.TODO() | ||||
|  | ||||
| 	// TODO: parse out --header or --metadata | ||||
|  | ||||
| 	// construct and execute the request using the json content type | ||||
| 	req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json")) | ||||
| 	var rsp json.RawMessage | ||||
|  | ||||
| 	if err := client.DefaultClient.Call(callCtx, req, &rsp); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// format the response | ||||
| 	var out bytes.Buffer | ||||
| 	defer out.Reset() | ||||
| 	if err := json.Indent(&out, rsp, "", "\t"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	out.Write([]byte("\n")) | ||||
| 	out.WriteTo(os.Stdout) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // splitCmdArgs takes a cli context and parses out the args and flags, for | ||||
| // example "micro helloworld --name=foo call apple" would result in "call", | ||||
| // "apple" as args and {"name":"foo"} as the flags. | ||||
| func splitCmdArgs(arguments []string) ([]string, map[string][]string, error) { | ||||
| 	args := []string{} | ||||
| 	flags := map[string][]string{} | ||||
|  | ||||
| 	prev := "" | ||||
| 	for _, a := range arguments { | ||||
| 		if !strings.HasPrefix(a, "--") { | ||||
| 			if len(prev) == 0 { | ||||
| 				args = append(args, a) | ||||
| 				continue | ||||
| 			} | ||||
| 			_, exists := flags[prev] | ||||
| 			if !exists { | ||||
| 				flags[prev] = []string{} | ||||
| 			} | ||||
|  | ||||
| 			flags[prev] = append(flags[prev], a) | ||||
| 			prev = "" | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// comps would be "foo", "bar" for "--foo=bar" | ||||
| 		comps := strings.Split(strings.TrimPrefix(a, "--"), "=") | ||||
| 		_, exists := flags[comps[0]] | ||||
| 		if !exists { | ||||
| 			flags[comps[0]] = []string{} | ||||
| 		} | ||||
| 		switch len(comps) { | ||||
| 		case 1: | ||||
| 			prev = comps[0] | ||||
| 		case 2: | ||||
| 			flags[comps[0]] = append(flags[comps[0]], comps[1]) | ||||
| 		default: | ||||
| 			return nil, nil, fmt.Errorf("Invalid flag: %v. Expected format: --foo=bar", a) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return args, flags, nil | ||||
| } | ||||
|  | ||||
| // constructEndpoint takes a slice of args and converts it into a valid endpoint | ||||
| // such as Helloworld.Call or Foo.Bar, it will return an error if an invalid number | ||||
| // of arguments were provided | ||||
| func constructEndpoint(args []string) (string, error) { | ||||
| 	var epComps []string | ||||
| 	switch len(args) { | ||||
| 	case 1: | ||||
| 		epComps = append(args, "call") | ||||
| 	case 2: | ||||
| 		epComps = args | ||||
| 	case 3: | ||||
| 		epComps = args[1:3] | ||||
| 	default: | ||||
| 		return "", fmt.Errorf("Incorrect number of arguments") | ||||
| 	} | ||||
|  | ||||
| 	// transform the endpoint components, e.g ["helloworld", "call"] to the | ||||
| 	// endpoint name: "Helloworld.Call". | ||||
| 	return fmt.Sprintf("%v.%v", strings.Title(epComps[0]), strings.Title(epComps[1])), nil | ||||
| } | ||||
|  | ||||
| // ShouldRenderHelp returns true if the help flag was passed | ||||
| func ShouldRenderHelp(args []string) bool { | ||||
| 	args, flags, _ := splitCmdArgs(args) | ||||
|  | ||||
| 	// only 1 arg e.g micro helloworld | ||||
| 	if len(args) == 1 { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	for key := range flags { | ||||
| 		if key == "help" { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // FlagsToRequest parses a set of flags, e.g {name:"Foo", "options_surname","Bar"} and | ||||
| // converts it into a request body. If the key is not a valid object in the request, an | ||||
| // error will be returned. | ||||
| // | ||||
| // This function constructs []interface{} slices | ||||
| // as opposed to typed ([]string etc) slices for easier testing | ||||
| func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]interface{}, error) { | ||||
| 	coerceValue := func(valueType string, value []string) (interface{}, error) { | ||||
| 		switch valueType { | ||||
| 		case "bool": | ||||
| 			if len(value) == 0 || len(strings.TrimSpace(value[0])) == 0 { | ||||
| 				return true, nil | ||||
| 			} | ||||
| 			return strconv.ParseBool(value[0]) | ||||
| 		case "int32": | ||||
| 			i, err := strconv.Atoi(value[0]) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			if i < math.MinInt32 || i > math.MaxInt32 { | ||||
| 				return nil, fmt.Errorf("value out of range for int32: %d", i) | ||||
| 			} | ||||
| 			return int32(i), nil | ||||
| 		case "int64": | ||||
| 			return strconv.ParseInt(value[0], 0, 64) | ||||
| 		case "float64": | ||||
| 			return strconv.ParseFloat(value[0], 64) | ||||
| 		case "[]bool": | ||||
| 			// length is one if it's a `,` separated int slice | ||||
| 			if len(value) == 1 { | ||||
| 				value = strings.Split(value[0], ",") | ||||
| 			} | ||||
| 			ret := []interface{}{} | ||||
| 			for _, v := range value { | ||||
| 				i, err := strconv.ParseBool(v) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				ret = append(ret, i) | ||||
| 			} | ||||
| 			return ret, nil | ||||
| 		case "[]int32": | ||||
| 			// length is one if it's a `,` separated int slice | ||||
| 			if len(value) == 1 { | ||||
| 				value = strings.Split(value[0], ",") | ||||
| 			} | ||||
| 			ret := []interface{}{} | ||||
| 			for _, v := range value { | ||||
| 				i, err := strconv.Atoi(v) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				if i < math.MinInt32 || i > math.MaxInt32 { | ||||
| 					return nil, fmt.Errorf("value out of range for int32: %d", i) | ||||
| 				} | ||||
| 				ret = append(ret, int32(i)) | ||||
| 			} | ||||
| 			return ret, nil | ||||
| 		case "[]int64": | ||||
| 			// length is one if it's a `,` separated int slice | ||||
| 			if len(value) == 1 { | ||||
| 				value = strings.Split(value[0], ",") | ||||
| 			} | ||||
| 			ret := []interface{}{} | ||||
| 			for _, v := range value { | ||||
| 				i, err := strconv.ParseInt(v, 0, 64) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				ret = append(ret, i) | ||||
| 			} | ||||
| 			return ret, nil | ||||
| 		case "[]float64": | ||||
| 			// length is one if it's a `,` separated float slice | ||||
| 			if len(value) == 1 { | ||||
| 				value = strings.Split(value[0], ",") | ||||
| 			} | ||||
| 			ret := []interface{}{} | ||||
| 			for _, v := range value { | ||||
| 				i, err := strconv.ParseFloat(v, 64) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				ret = append(ret, i) | ||||
| 			} | ||||
| 			return ret, nil | ||||
| 		case "[]string": | ||||
| 			// length is one it's a `,` separated string slice | ||||
| 			if len(value) == 1 { | ||||
| 				value = strings.Split(value[0], ",") | ||||
| 			} | ||||
| 			ret := []interface{}{} | ||||
| 			for _, v := range value { | ||||
| 				ret = append(ret, v) | ||||
| 			} | ||||
| 			return ret, nil | ||||
| 		case "string": | ||||
| 			return value[0], nil | ||||
| 		case "map[string]string": | ||||
| 			var val map[string]string | ||||
| 			if err := json.Unmarshal([]byte(value[0]), &val); err != nil { | ||||
| 				return value[0], nil | ||||
| 			} | ||||
| 			return val, nil | ||||
| 		default: | ||||
| 			return value, nil | ||||
| 		} | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	result := objx.MustFromJSON("{}") | ||||
|  | ||||
| 	var flagType func(key string, values []*registry.Value, path ...string) (string, bool) | ||||
|  | ||||
| 	flagType = func(key string, values []*registry.Value, path ...string) (string, bool) { | ||||
| 		for _, attr := range values { | ||||
| 			if strings.Join(append(path, attr.Name), "-") == key { | ||||
| 				return attr.Type, true | ||||
| 			} | ||||
| 			if attr.Values != nil { | ||||
| 				typ, found := flagType(key, attr.Values, append(path, attr.Name)...) | ||||
| 				if found { | ||||
| 					return typ, found | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return "", false | ||||
| 	} | ||||
|  | ||||
| 	for key, value := range flags { | ||||
| 		ty, found := flagType(key, req.Values) | ||||
| 		if !found { | ||||
| 			return nil, fmt.Errorf("Unknown flag: %v", key) | ||||
| 		} | ||||
| 		parsed, err := coerceValue(ty, value) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// objx.Set does not create the path, | ||||
| 		// so we do that here | ||||
| 		if strings.Contains(key, "-") { | ||||
| 			parts := strings.Split(key, "-") | ||||
| 			for i, _ := range parts { | ||||
| 				pToCreate := strings.Join(parts[0:i], ".") | ||||
| 				if i > 0 && i < len(parts) && !result.Has(pToCreate) { | ||||
| 					result.Set(pToCreate, map[string]interface{}{}) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		path := strings.Replace(key, "-", ".", -1) | ||||
| 		result.Set(path, parsed) | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // find a service in a domain matching the name | ||||
| func serviceWithName(name string) (*registry.Service, error) { | ||||
| 	srvs, err := registry.GetService(name) | ||||
| 	if err == registry.ErrNotFound { | ||||
| 		return nil, nil | ||||
| 	} else if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(srvs) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return srvs[0], nil | ||||
| } | ||||
							
								
								
									
										379
									
								
								cmd/micro/cli/util/dynamic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								cmd/micro/cli/util/dynamic_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,379 @@ | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
| 	goregistry "go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type parseCase struct { | ||||
| 	args     []string | ||||
| 	values   *goregistry.Value | ||||
| 	expected map[string]interface{} | ||||
| } | ||||
|  | ||||
| func TestDynamicFlagParsing(t *testing.T) { | ||||
| 	cases := []parseCase{ | ||||
| 		{ | ||||
| 			args: []string{"--ss=a,b"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "ss", | ||||
| 						Type: "[]string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"ss": []interface{}{"a", "b"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--ss", "a,b"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "ss", | ||||
| 						Type: "[]string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"ss": []interface{}{"a", "b"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--ss=a", "--ss=b"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "ss", | ||||
| 						Type: "[]string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"ss": []interface{}{"a", "b"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--ss", "a", "--ss", "b"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "ss", | ||||
| 						Type: "[]string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"ss": []interface{}{"a", "b"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--bs=true,false"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "bs", | ||||
| 						Type: "[]bool", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"bs": []interface{}{true, false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--bs", "true,false"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "bs", | ||||
| 						Type: "[]bool", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"bs": []interface{}{true, false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--bs=true", "--bs=false"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "bs", | ||||
| 						Type: "[]bool", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"bs": []interface{}{true, false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--bs", "true", "--bs", "false"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "bs", | ||||
| 						Type: "[]bool", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"bs": []interface{}{true, false}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is=10,20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int32", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int32(10), int32(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is", "10,20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int32", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int32(10), int32(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is=10", "--is=20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int32", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int32(10), int32(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is", "10", "--is", "20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int32", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int32(10), int32(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is=10,20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int64(10), int64(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is", "10,20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int64(10), int64(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is=10", "--is=20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int64(10), int64(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--is", "10", "--is", "20"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "is", | ||||
| 						Type: "[]int64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"is": []interface{}{int64(10), int64(20)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--fs=10.1,20.2"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "fs", | ||||
| 						Type: "[]float64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"fs": []interface{}{float64(10.1), float64(20.2)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--fs", "10.1,20.2"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "fs", | ||||
| 						Type: "[]float64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"fs": []interface{}{float64(10.1), float64(20.2)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--fs=10.1", "--fs=20.2"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "fs", | ||||
| 						Type: "[]float64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"fs": []interface{}{float64(10.1), float64(20.2)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--fs", "10.1", "--fs", "20.2"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "fs", | ||||
| 						Type: "[]float64", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"fs": []interface{}{float64(10.1), float64(20.2)}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--user_email=someemail"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "user_email", | ||||
| 						Type: "string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"user_email": "someemail", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--user_email=someemail", "--user_name=somename"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "user_email", | ||||
| 						Type: "string", | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "user_name", | ||||
| 						Type: "string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"user_email": "someemail", | ||||
| 				"user_name":  "somename", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--b"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "b", | ||||
| 						Type: "bool", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"b": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			args: []string{"--user_friend_email=hi"}, | ||||
| 			values: &goregistry.Value{ | ||||
| 				Values: []*goregistry.Value{ | ||||
| 					{ | ||||
| 						Name: "user_friend_email", | ||||
| 						Type: "string", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: map[string]interface{}{ | ||||
| 				"user_friend_email": "hi", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, c := range cases { | ||||
| 		t.Run(strings.Join(c.args, " "), func(t *testing.T) { | ||||
| 			_, flags, err := splitCmdArgs(c.args) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			req, err := FlagsToRequest(flags, c.values) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(c.expected, req) { | ||||
| 				spew.Dump("Expected:", c.expected, "got: ", req) | ||||
| 				t.Fatalf("Expected %v, got %v", c.expected, req) | ||||
| 			} | ||||
| 		}) | ||||
|  | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										72
									
								
								cmd/micro/cli/util/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								cmd/micro/cli/util/util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // Package cliutil contains methods used across all cli commands | ||||
| // @todo: get rid of os.Exits and use errors instread | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	merrors "go-micro.dev/v5/errors" | ||||
| ) | ||||
|  | ||||
| type Exec func(*cli.Context, []string) ([]byte, error) | ||||
|  | ||||
| func Print(e Exec) func(*cli.Context) error { | ||||
| 	return func(c *cli.Context) error { | ||||
| 		rsp, err := e(c, c.Args().Slice()) | ||||
| 		if err != nil { | ||||
| 			return CliError(err) | ||||
| 		} | ||||
| 		if len(rsp) > 0 { | ||||
| 			fmt.Printf("%s\n", string(rsp)) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CliError returns a user friendly message from error. If we can't determine a good one returns an error with code 128 | ||||
| func CliError(err error) cli.ExitCoder { | ||||
| 	if err == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	// if it's already a cli.ExitCoder we use this | ||||
| 	cerr, ok := err.(cli.ExitCoder) | ||||
| 	if ok { | ||||
| 		return cerr | ||||
| 	} | ||||
|  | ||||
| 	// grpc errors | ||||
| 	if mname := regexp.MustCompile(`malformed method name: \\?"(\w+)\\?"`).FindStringSubmatch(err.Error()); len(mname) > 0 { | ||||
| 		return cli.Exit(fmt.Sprintf(`Method name "%s" invalid format. Expecting service.endpoint`, mname[1]), 3) | ||||
| 	} | ||||
| 	if service := regexp.MustCompile(`service ([\w\.]+): route not found`).FindStringSubmatch(err.Error()); len(service) > 0 { | ||||
| 		return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 4) | ||||
| 	} | ||||
| 	if service := regexp.MustCompile(`unknown service ([\w\.]+)`).FindStringSubmatch(err.Error()); len(service) > 0 { | ||||
| 		if strings.Contains(service[0], ".") { | ||||
| 			return cli.Exit(fmt.Sprintf(`Service method "%s" not found`, service[1]), 5) | ||||
| 		} | ||||
| 		return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 5) | ||||
| 	} | ||||
| 	if address := regexp.MustCompile(`Error while dialing dial tcp.*?([\w]+\.[\w:\.]+): `).FindStringSubmatch(err.Error()); len(address) > 0 { | ||||
| 		return cli.Exit(fmt.Sprintf(`Failed to connect to micro server at %s`, address[1]), 4) | ||||
| 	} | ||||
|  | ||||
| 	merr, ok := err.(*merrors.Error) | ||||
| 	if !ok { | ||||
| 		return cli.Exit(err, 128) | ||||
| 	} | ||||
|  | ||||
| 	switch merr.Code { | ||||
| 	case 408: | ||||
| 		return cli.Exit("Request timed out", 1) | ||||
| 	case 401: | ||||
| 		// TODO check if not signed in, prompt to sign in | ||||
| 		return cli.Exit("Not authorized to perform this request", 2) | ||||
| 	} | ||||
|  | ||||
| 	// fallback to using the detail from the merr | ||||
| 	return cli.Exit(merr.Detail, 127) | ||||
| } | ||||
							
								
								
									
										26
									
								
								cmd/micro/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cmd/micro/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"embed" | ||||
| 	"go-micro.dev/v5/cmd" | ||||
|  | ||||
| 	_ "go-micro.dev/v5/cmd/micro/cli" | ||||
| 	_ "go-micro.dev/v5/cmd/micro/run" | ||||
| 	"go-micro.dev/v5/cmd/micro/server" | ||||
| ) | ||||
|  | ||||
| //go:embed web/styles.css web/main.js web/templates/* | ||||
| var webFS embed.FS | ||||
|  | ||||
| var version = "5.0.0-dev" | ||||
|  | ||||
| func init() { | ||||
| 	server.HTML = webFS | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	cmd.Init( | ||||
| 		cmd.Name("micro"), | ||||
| 		cmd.Version(version), | ||||
| 	) | ||||
| } | ||||
							
								
								
									
										227
									
								
								cmd/micro/run/run.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								cmd/micro/run/run.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| package run | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"crypto/md5" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/signal" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/cmd" | ||||
| ) | ||||
|  | ||||
| // Color codes for log output | ||||
| var colors = []string{ | ||||
| 	"\033[31m", // red | ||||
| 	"\033[32m", // green | ||||
| 	"\033[33m", // yellow | ||||
| 	"\033[34m", // blue | ||||
| 	"\033[35m", // magenta | ||||
| 	"\033[36m", // cyan | ||||
| } | ||||
|  | ||||
| func colorFor(idx int) string { | ||||
| 	return colors[idx%len(colors)] | ||||
| } | ||||
|  | ||||
| func Run(c *cli.Context) error { | ||||
| 	dir := c.Args().Get(0) | ||||
| 	var tmpDir string | ||||
| 	if len(dir) == 0 { | ||||
| 		dir = "." | ||||
| 	} else if strings.HasPrefix(dir, "github.com/") || strings.HasPrefix(dir, "https://github.com/") { | ||||
| 		// Handle git URLs | ||||
| 		repo := dir | ||||
| 		if strings.HasPrefix(repo, "https://") { | ||||
| 			repo = strings.TrimPrefix(repo, "https://") | ||||
| 		} | ||||
| 		// Clone to a temp directory | ||||
| 		tmp, err := os.MkdirTemp("", "micro-run-") | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to create temp dir: %w", err) | ||||
| 		} | ||||
| 		tmpDir = tmp | ||||
| 		cloneURL := repo | ||||
| 		if !strings.HasPrefix(cloneURL, "https://") { | ||||
| 			cloneURL = "https://" + repo | ||||
| 		} | ||||
| 		// Run git clone | ||||
| 		cmd := exec.Command("git", "clone", cloneURL, tmpDir) | ||||
| 		cmd.Stdout = os.Stdout | ||||
| 		cmd.Stderr = os.Stderr | ||||
| 		if err := cmd.Run(); err != nil { | ||||
| 			return fmt.Errorf("failed to clone repo %s: %w", cloneURL, err) | ||||
| 		} | ||||
| 		dir = tmpDir | ||||
| 	} | ||||
|  | ||||
| 	homeDir, err := os.UserHomeDir() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to get home dir: %w", err) | ||||
| 	} | ||||
| 	logsDir := filepath.Join(homeDir, "micro", "logs") | ||||
| 	if err := os.MkdirAll(logsDir, 0755); err != nil { | ||||
| 		return fmt.Errorf("failed to create logs dir: %w", err) | ||||
| 	} | ||||
| 	runDir := filepath.Join(homeDir, "micro", "run") | ||||
| 	if err := os.MkdirAll(runDir, 0755); err != nil { | ||||
| 		return fmt.Errorf("failed to create run dir: %w", err) | ||||
| 	} | ||||
| 	binDir := filepath.Join(homeDir, "micro", "bin") | ||||
| 	if err := os.MkdirAll(binDir, 0755); err != nil { | ||||
| 		return fmt.Errorf("failed to create bin dir: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Always run all services (find all main.go) | ||||
| 	var mainFiles []string | ||||
| 	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if info.IsDir() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		if info.Name() == "main.go" { | ||||
| 			mainFiles = append(mainFiles, path) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error walking the path: %w", err) | ||||
| 	} | ||||
| 	if len(mainFiles) == 0 { | ||||
| 		return fmt.Errorf("no main.go files found in %s", dir) | ||||
| 	} | ||||
| 	var procs []*exec.Cmd | ||||
| 	var pidFiles []string | ||||
| 	for i, mainFile := range mainFiles { | ||||
| 		serviceDir := filepath.Dir(mainFile) | ||||
| 		var serviceName string | ||||
| 		absServiceDir, _ := filepath.Abs(serviceDir) | ||||
| 		// Determine service name: if absServiceDir matches the provided dir (which may be "."), use cwd | ||||
| 		if absServiceDir == dir { | ||||
| 			cwd, _ := os.Getwd() | ||||
| 			serviceName = filepath.Base(cwd) | ||||
| 		} else { | ||||
| 			serviceName = filepath.Base(serviceDir) | ||||
| 		} | ||||
| 		serviceNameForPid := serviceName + "-" + fmt.Sprintf("%x", md5.Sum([]byte(absServiceDir)))[:8] | ||||
| 		logFilePath := filepath.Join(logsDir, serviceNameForPid+".log") | ||||
| 		binPath := filepath.Join(binDir, serviceNameForPid) | ||||
| 		pidFilePath := filepath.Join(runDir, serviceNameForPid+".pid") | ||||
|  | ||||
| 		// Check if pid file exists and process is running | ||||
| 		if pidBytes, err := os.ReadFile(pidFilePath); err == nil { | ||||
| 			lines := strings.Split(string(pidBytes), "\n") | ||||
| 			if len(lines) > 0 && len(lines[0]) > 0 { | ||||
| 				pid := lines[0] | ||||
| 				if _, err := os.FindProcess(parsePid(pid)); err == nil { | ||||
| 					if processRunning(pid) { | ||||
| 						fmt.Fprintf(os.Stderr, "Service %s already running (pid %s)\n", serviceNameForPid, pid) | ||||
| 						continue | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "failed to open log file for %s: %v\n", serviceName, err) | ||||
| 			continue | ||||
| 		} | ||||
| 		buildCmd := exec.Command("go", "build", "-o", binPath, ".") | ||||
| 		buildCmd.Dir = serviceDir | ||||
| 		buildOut, buildErr := buildCmd.CombinedOutput() | ||||
| 		if buildErr != nil { | ||||
| 			logFile.WriteString(string(buildOut)) | ||||
| 			logFile.Close() | ||||
| 			fmt.Fprintf(os.Stderr, "failed to build %s: %v\n", serviceName, buildErr) | ||||
| 			continue | ||||
| 		} | ||||
| 		cmd := exec.Command(binPath) | ||||
| 		cmd.Dir = serviceDir | ||||
| 		pr, pw := io.Pipe() | ||||
| 		cmd.Stdout = pw | ||||
| 		cmd.Stderr = pw | ||||
| 		color := colorFor(i) | ||||
| 		go func(name string, color string, pr *io.PipeReader, logFile *os.File) { | ||||
| 			defer logFile.Close() | ||||
| 			scanner := bufio.NewScanner(pr) | ||||
| 			for scanner.Scan() { | ||||
| 				line := scanner.Text() | ||||
| 				// Write to terminal with color and service name | ||||
| 				fmt.Printf("%s[%s]\033[0m %s\n", color, name, line) | ||||
| 				// Write to log file with service name prefix | ||||
| 				logFile.WriteString("[" + name + "] " + line + "\n") | ||||
| 			} | ||||
| 		}(serviceName, color, pr, logFile) | ||||
| 		if err := cmd.Start(); err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "failed to start service %s: %v\n", serviceName, err) | ||||
| 			pw.Close() | ||||
| 			continue | ||||
| 		} | ||||
| 		procs = append(procs, cmd) | ||||
| 		pidFiles = append(pidFiles, pidFilePath) | ||||
| 		os.WriteFile(pidFilePath, []byte(fmt.Sprintf("%d\n%s\n%s\n%s\n", cmd.Process.Pid, absServiceDir, serviceName, time.Now().Format(time.RFC3339))), 0644) | ||||
| 	} | ||||
| 	ch := make(chan os.Signal, 1) | ||||
| 	signal.Notify(ch, os.Interrupt) | ||||
| 	go func() { | ||||
| 		<-ch | ||||
| 		for _, proc := range procs { | ||||
| 			if proc.Process != nil { | ||||
| 				_ = proc.Process.Kill() | ||||
| 			} | ||||
| 		} | ||||
| 		for _, pf := range pidFiles { | ||||
| 			_ = os.Remove(pf) | ||||
| 		} | ||||
| 		os.Exit(1) | ||||
| 	}() | ||||
| 	for _, proc := range procs { | ||||
| 		_ = proc.Wait() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Add helpers for process check | ||||
| func parsePid(pidStr string) int { | ||||
| 	pid, _ := strconv.Atoi(pidStr) | ||||
| 	return pid | ||||
| } | ||||
| func processRunning(pidStr string) bool { | ||||
| 	pid := parsePid(pidStr) | ||||
| 	if pid <= 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 	proc, err := os.FindProcess(pid) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	// On Unix, sending signal 0 checks if process exists | ||||
| 	return proc.Signal(syscall.Signal(0)) == nil | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	cmd.Register(&cli.Command{ | ||||
| 		Name:   "run", | ||||
| 		Usage:  "Run all services in a directory", | ||||
| 		Action: Run, | ||||
| 		Flags: []cli.Flag{ | ||||
| 			&cli.StringFlag{ | ||||
| 				Name:    "address", | ||||
| 				Aliases: []string{"a"}, | ||||
| 				Usage:   "Address to bind the micro web UI (default :8080)", | ||||
| 				Value:   ":8080", | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										1072
									
								
								cmd/micro/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1072
									
								
								cmd/micro/server/server.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										91
									
								
								cmd/micro/server/util_jwt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								cmd/micro/server/util_jwt.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	jwtPrivateKey *rsa.PrivateKey | ||||
| 	jwtPublicKey  *rsa.PublicKey | ||||
| ) | ||||
|  | ||||
| // Load or generate RSA keys for JWT | ||||
| func InitJWTKeys(privPath, pubPath string) error { | ||||
| 	var err error | ||||
| 	if _, err = os.Stat(privPath); os.IsNotExist(err) { | ||||
| 		priv, _ := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 		privBytes := x509.MarshalPKCS1PrivateKey(priv) | ||||
| 		privPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) | ||||
| 		os.WriteFile(privPath, privPem, 0600) | ||||
| 		pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) | ||||
| 		pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}) | ||||
| 		os.WriteFile(pubPath, pubPem, 0644) | ||||
| 	} | ||||
| 	privPem, err := os.ReadFile(privPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	block, _ := pem.Decode(privPem) | ||||
| 	if block == nil { | ||||
| 		return errors.New("invalid private key PEM") | ||||
| 	} | ||||
| 	jwtPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	pubPem, err := os.ReadFile(pubPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	block, _ = pem.Decode(pubPem) | ||||
| 	if block == nil { | ||||
| 		return errors.New("invalid public key PEM") | ||||
| 	} | ||||
| 	pub, err := x509.ParsePKIXPublicKey(block.Bytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	var ok bool | ||||
| 	jwtPublicKey, ok = pub.(*rsa.PublicKey) | ||||
| 	if !ok { | ||||
| 		return errors.New("not RSA public key") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Generate a JWT for a user | ||||
| func GenerateJWT(userID, userType string, scopes []string, expiry time.Duration) (string, error) { | ||||
| 	claims := jwt.MapClaims{ | ||||
| 		"sub":    userID, | ||||
| 		"type":   userType, | ||||
| 		"scopes": scopes, | ||||
| 		"exp":    time.Now().Add(expiry).Unix(), | ||||
| 	} | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) | ||||
| 	return token.SignedString(jwtPrivateKey) | ||||
| } | ||||
|  | ||||
| // Parse and validate a JWT, returns claims if valid | ||||
| func ParseJWT(tokenStr string) (jwt.MapClaims, error) { | ||||
| 	token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { | ||||
| 		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { | ||||
| 			return nil, errors.New("unexpected signing method") | ||||
| 		} | ||||
| 		return jwtPublicKey, nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { | ||||
| 		return claims, nil | ||||
| 	} | ||||
| 	return nil, errors.New("invalid token") | ||||
| } | ||||
							
								
								
									
										34
									
								
								cmd/micro/web/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								cmd/micro/web/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| // Minimal JS for reactive form submissions | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     document.querySelectorAll('form[data-reactive]')?.forEach(function(form) { | ||||
|         form.addEventListener('submit', async function(e) { | ||||
|             e.preventDefault(); | ||||
|             const formData = new FormData(form); | ||||
|             const params = {}; | ||||
|             for (const [key, value] of formData.entries()) { | ||||
|                 params[key] = value; | ||||
|             } | ||||
|             const action = form.getAttribute('action'); | ||||
|             const method = form.getAttribute('method') || 'POST'; | ||||
|             try { | ||||
|                 const resp = await fetch(action, { | ||||
|                     method, | ||||
|                     headers: { 'Content-Type': 'application/json' }, | ||||
|                     body: JSON.stringify(params) | ||||
|                 }); | ||||
|                 const data = await resp.json(); | ||||
|                 // Find or create a response container | ||||
|                 let respDiv = form.querySelector('.js-response'); | ||||
|                 if (!respDiv) { | ||||
|                     respDiv = document.createElement('div'); | ||||
|                     respDiv.className = 'js-response'; | ||||
|                     form.appendChild(respDiv); | ||||
|                 } | ||||
|                 respDiv.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>'; | ||||
|             } catch (err) { | ||||
|                 alert('Error: ' + err); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										236
									
								
								cmd/micro/web/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								cmd/micro/web/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| body { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   font-family: 'Inter', 'Segoe UI', 'Arial', 'Helvetica Neue', Arial, sans-serif; | ||||
|   font-size: 15px; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   line-height: 1.7; | ||||
| } | ||||
|  | ||||
| header, nav, footer { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   padding: 1.2em 2em 1.2em 2em; | ||||
|   margin-bottom: 2em; | ||||
| } | ||||
| nav { | ||||
|   margin: 20px; | ||||
|   border-radius: 20px; | ||||
|  | ||||
| } | ||||
| main { | ||||
|   max-width: 1400px; | ||||
|   margin: 0 auto; | ||||
|   padding: 2em 1em 3em 1em; | ||||
|   background: #fff; | ||||
|   margin-left: 100px; /* leave space for sidebar */ | ||||
|   margin-right: 100px; | ||||
| } | ||||
|  | ||||
| h1, h2, h3, h4, h5, h6 { | ||||
|   color: #111; | ||||
|   font-weight: 600; | ||||
|   margin-top: 2em; | ||||
|   margin-bottom: 0.5em; | ||||
|   letter-spacing: -0.01em; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 2.2em; | ||||
|   margin-top: 0; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   font-size: 1.4em; | ||||
| } | ||||
|  | ||||
| hr { | ||||
|   border: none; | ||||
|   border-top: 1px solid #222; | ||||
|   margin: 2em 0; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   color: #111; | ||||
|   text-decoration: none; | ||||
|   transition: background 0.2s; | ||||
| } | ||||
| a:hover { | ||||
|   font-weight: bold; | ||||
| } | ||||
|  | ||||
| ul, ol { | ||||
|   margin: 1em 0 1em 2em; | ||||
|   padding: 0; | ||||
| } | ||||
|  | ||||
| li { | ||||
|   margin-bottom: 0.5em; | ||||
| } | ||||
|  | ||||
| pre, code { | ||||
|   background: #f7f7f7; | ||||
|   color: #111; | ||||
|   font-family: inherit; | ||||
|   font-size: 0.98em; | ||||
|   border-radius: 5px; | ||||
|   padding: 0.2em 0.4em; | ||||
| } | ||||
|  | ||||
| pre { | ||||
|   padding: 1em; | ||||
|   overflow-x: auto; | ||||
|   border-radius: 0; | ||||
|   margin: 1.5em 0; | ||||
| } | ||||
|  | ||||
| form { | ||||
|   background: #fff; | ||||
|   border: 1px solid #222; | ||||
|   padding: 1.5em 1.5em 1em 1.5em; | ||||
|   margin: 2em 0; | ||||
|   border-radius: 10px; | ||||
|   box-shadow: none; | ||||
| } | ||||
|  | ||||
| input, select, textarea { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   border: 1px solid #222; | ||||
|   border-radius: 7px; | ||||
|   font-size: 1em; | ||||
|   padding: 0.5em 0.7em; | ||||
|   margin-bottom: 1em; | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
|   outline: none; | ||||
|   transition: border 0.2s; | ||||
| } | ||||
| input:focus, select:focus, textarea:focus { | ||||
|   border: 1.5px solid #111; | ||||
| } | ||||
|  | ||||
| button, input[type="submit"], .button { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   border: 1.5px solid #111; | ||||
|   border-radius: 7px; | ||||
|   font-size: 1em; | ||||
|   padding: 0.5em 1.2em; | ||||
|   margin: 0.5em 0.2em 0.5em 0; | ||||
|   cursor: pointer; | ||||
|   font-family: inherit; | ||||
|   transition: background 0.2s, color 0.2s; | ||||
| } | ||||
| button:hover, input[type="submit"]:hover, .button:hover { | ||||
|   background: #111; | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| .table, table { | ||||
|   width: 100%; | ||||
|   border-collapse: collapse; | ||||
|   background: #fff; | ||||
|   margin: 2em 0; | ||||
| } | ||||
| table th, table td { | ||||
|   border: none; | ||||
|   padding: 0.7em 1em; | ||||
|   text-align: left; | ||||
| } | ||||
| table th { | ||||
|   background: #f7f7f7; | ||||
|   color: #111; | ||||
|   font-weight: 600; | ||||
| } | ||||
| table tr:nth-child(even) { | ||||
|   background: #f7f7f7; | ||||
| } | ||||
|  | ||||
| .no-bullets { | ||||
|   list-style: none; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
|  | ||||
| .copy-btn { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   border: 1px solid #222; | ||||
|   border-radius: 7px; | ||||
|   font-size: 0.95em; | ||||
|   padding: 0.2em 0.7em; | ||||
|   margin-left: 0.5em; | ||||
|   cursor: pointer; | ||||
|   transition: background 0.2s, color 0.2s; | ||||
| } | ||||
| .copy-btn:hover { | ||||
|   background: #111; | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| .alert, .error, .success { | ||||
|   background: #fff; | ||||
|   color: #111; | ||||
|   border: 1px solid #222; | ||||
|   padding: 1em 1.5em; | ||||
|   margin: 2em 0; | ||||
|   border-radius: 10px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar { | ||||
|   width: 8px; | ||||
|   background: #fff; | ||||
| } | ||||
| ::-webkit-scrollbar-thumb { | ||||
|   background: #222; | ||||
| } | ||||
|  | ||||
| @media (max-width: 800px) { | ||||
|   main { | ||||
|     max-width: 98vw; | ||||
|     padding: 1em 0.2em 2em 0.2em; | ||||
|     margin-left: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* Inline/unstyled form for delete button */ | ||||
| .form-inline, .form-plain { | ||||
|   display: inline; | ||||
|   background: none; | ||||
|   border: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
|   box-shadow: none; | ||||
| } | ||||
| .form-inline input, .form-inline button, .form-plain input, .form-plain button { | ||||
|   margin: 0; | ||||
|   padding: 0.3em 1em; | ||||
|   border-radius: 7px; | ||||
|   font-size: 1em; | ||||
| } | ||||
| .delete-btn, .form-inline .delete-btn, .form-plain .delete-btn { | ||||
|   background: #fff; | ||||
|   color: #c00; | ||||
|   border: 1.5px solid #c00; | ||||
|   border-radius: 7px; | ||||
|   font-size: 1em; | ||||
|   padding: 0.3em 1em; | ||||
|   margin: 0 0.2em; | ||||
|   cursor: pointer; | ||||
|   font-family: inherit; | ||||
|   transition: background 0.2s, color 0.2s; | ||||
| } | ||||
| .delete-btn:hover { | ||||
|   background: #c00; | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| #title { | ||||
|   text-decoration: none; | ||||
| } | ||||
| .log-link:hover { | ||||
|   font-weight: normal; | ||||
|   text-decoration: underline; | ||||
| } | ||||
							
								
								
									
										34
									
								
								cmd/micro/web/templates/api.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								cmd/micro/web/templates/api.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">API</h2> | ||||
| <p class="api-auth-info" style="background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.08em; border-radius:6px;"> | ||||
|   <b>API Authentication Required:</b> All API calls to <code>/api/...</code> endpoints (except this page) must include an <b>Authorization: Bearer <token></b> header.<br> | ||||
|   You can generate tokens on the <a href="/auth/tokens">Tokens page</a>. | ||||
| </p> | ||||
| {{range .Services}} | ||||
|   <h3 id="{{.Anchor}}" style="margin-top:3em; font-size:1.2em; font-weight:bold;">{{.Name}}</h3> | ||||
|   {{if .Endpoints}} | ||||
|     <div style="margin-bottom:3em;"> | ||||
|     {{range .Endpoints}} | ||||
|       <div style="margin-bottom:2.8em; padding:1.3em 1.5em; background:#fafbfc; border-radius:7px; border:1px solid #eee;"> | ||||
|         <div style="font-size:1.12em; margin-bottom:0.7em;"><a href="{{.Path}}" class="micro-link" style="font-weight:bold;">{{.Name}}</a></div> | ||||
|         <div style="margin-bottom:0.8em; color:#888; font-size:1em;"> | ||||
|           <b>HTTP Path:</b> <code>{{.Path}}</code> | ||||
|         </div> | ||||
|         <div style="display:flex; gap:3em; flex-wrap:wrap;"> | ||||
|           <div style="min-width:240px;"> | ||||
|             <b>Request:</b> | ||||
|             <pre style="background:#f4f4f4; border-radius:5px; padding:1em 1.2em; margin:0.5em 0 1em 0; font-size:1em;">{{.Params}}</pre> | ||||
|           </div> | ||||
|           <div style="min-width:240px;"> | ||||
|             <b>Response:</b> | ||||
|             <pre style="background:#f4f4f4; border-radius:5px; padding:1em 1.2em; margin:0.5em 0 1em 0; font-size:1em;">{{.Response}}</pre> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     {{end}} | ||||
|     </div> | ||||
|   {{else}} | ||||
|     <p style="color:#888;">No endpoints</p> | ||||
|   {{end}} | ||||
| {{end}} | ||||
| {{end}} | ||||
							
								
								
									
										15
									
								
								cmd/micro/web/templates/auth_login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cmd/micro/web/templates/auth_login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Login</h2> | ||||
| <form method="POST" action="/auth/login" style="max-width:340px; margin:2em 0;"> | ||||
|   <div style="margin-bottom:1.2em;"> | ||||
|     <input name="id" placeholder="Username" required style="width:100%; padding:0.7em;"> | ||||
|   </div> | ||||
|   <div style="margin-bottom:1.2em;"> | ||||
|     <input name="password" type="password" placeholder="Password" required style="width:100%; padding:0.7em;"> | ||||
|   </div> | ||||
|   <button type="submit" style="width:100%; padding:0.7em;">Login</button> | ||||
| </form> | ||||
| {{if .Error}} | ||||
|   <div style="color:#c00; margin-top:1em;">{{.Error}}</div> | ||||
| {{end}} | ||||
| {{end}} | ||||
							
								
								
									
										63
									
								
								cmd/micro/web/templates/auth_tokens.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								cmd/micro/web/templates/auth_tokens.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Auth Tokens</h2> | ||||
| <table style="margin-bottom:2em;"> | ||||
|   <thead> | ||||
|     <tr><th>ID</th><th>Type</th><th>Scopes</th><th>Metadata</th><th>Token</th><th>Delete</th></tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     {{range .Tokens}} | ||||
|     <tr> | ||||
|       <td>{{.ID}}</td> | ||||
|       <td>{{.Type}}</td> | ||||
|       <td>{{range .Scopes}}<code>{{.}}</code> {{end}}</td> | ||||
|       <td> | ||||
|         {{range $k, $v := .Metadata}} | ||||
|           {{if and (ne $k "password_hash") (ne $k "token")}} | ||||
|             <b>{{$k}}</b>: {{$v}}  | ||||
|           {{end}} | ||||
|         {{end}} | ||||
|       </td> | ||||
|       <td style="max-width:320px; word-break:break-all;"> | ||||
|         {{if .Token}} | ||||
|           <span class="obfuscated-token" data-token="{{.Token}}"> | ||||
|             {{if .TokenSuffix}} | ||||
|               {{.TokenPrefix}}...{{.TokenSuffix}} | ||||
|             {{else}} | ||||
|               {{.Token}} | ||||
|             {{end}} | ||||
|           </span> | ||||
|           <button onclick="copyToken(this)" data-token="{{.Token}}" style="margin-left:0.5em;">Copy</button> | ||||
|         {{end}} | ||||
|       </td> | ||||
|       <td> | ||||
|         <form method="POST" action="/auth/tokens" style="display:inline; padding: 0; border: 0"> | ||||
|           <input type="hidden" name="delete" value="{{.ID}}"> | ||||
|           <button type="submit" onclick="return confirm('Delete token {{.ID}}?')">Delete</button> | ||||
|         </form> | ||||
|       </td> | ||||
|     </tr> | ||||
|     {{end}} | ||||
|   </tbody> | ||||
| </table> | ||||
| <h3 style="margin-bottom:1em;">Create New Token</h3> | ||||
| <form method="POST" action="/auth/tokens"> | ||||
|   <input name="id" placeholder="Name/ID" required style="margin-right:1em;"> | ||||
|   <select name="type" style="margin-right:1em;"> | ||||
|     <option value="user">User</option> | ||||
|     <option value="admin">Admin</option> | ||||
|     <option value="service">Service</option> | ||||
|   </select> | ||||
|   <input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:1em;"> | ||||
|   <button type="submit">Create</button> | ||||
| </form> | ||||
| <script> | ||||
| function copyToken(btn) { | ||||
|   const token = btn.getAttribute('data-token'); | ||||
|   if (navigator.clipboard) { | ||||
|     navigator.clipboard.writeText(token); | ||||
|     btn.textContent = 'Copied!'; | ||||
|     setTimeout(() => { btn.textContent = 'Copy'; }, 1200); | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| {{end}} | ||||
							
								
								
									
										40
									
								
								cmd/micro/web/templates/auth_users.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								cmd/micro/web/templates/auth_users.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">User Accounts</h2> | ||||
| <table style="margin-bottom:2em;"> | ||||
|   <thead> | ||||
|     <tr><th>ID</th><th>Type</th><th>Scopes</th><th>Metadata</th><th>Delete</th></tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     {{range .Users}} | ||||
|     <tr> | ||||
|       <td>{{.ID}}</td> | ||||
|       <td>{{.Type}}</td> | ||||
|       <td>{{range .Scopes}}<code>{{.}}</code> {{end}}</td> | ||||
|       <td> | ||||
|         {{range $k, $v := .Metadata}} | ||||
|           {{if ne $k "password_hash"}} | ||||
|             <b>{{$k}}</b>: {{$v}}  | ||||
|           {{end}} | ||||
|         {{end}} | ||||
|       </td> | ||||
|       <td> | ||||
|         <form method="POST" action="/auth/users" style="display:inline; padding: 0; border: 0"> | ||||
|           <input type="hidden" name="delete" value="{{.ID}}"> | ||||
|           <button type="submit" onclick="return confirm('Delete user {{.ID}}?')">Delete</button> | ||||
|         </form> | ||||
|       </td> | ||||
|     </tr> | ||||
|     {{end}} | ||||
|   </tbody> | ||||
| </table> | ||||
| <h3 style="margin-bottom:1em;">Create New User</h3> | ||||
| <form method="POST" action="/auth/users"> | ||||
|   <input name="id" placeholder="Username" required style="margin-right:1em;"> | ||||
|   <input name="password" type="password" placeholder="Password" required style="margin-right:1em;"> | ||||
|   <select name="type" style="margin-right:1em;"> | ||||
|     <option value="user">User</option> | ||||
|     <option value="admin">Admin</option> | ||||
|   </select> | ||||
|   <button type="submit">Create</button> | ||||
| </form> | ||||
| {{end}} | ||||
							
								
								
									
										52
									
								
								cmd/micro/web/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								cmd/micro/web/templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width"> | ||||
|     <title>{{.Title}}</title> | ||||
|     <link rel="stylesheet" href="/styles.css"> | ||||
| </head> | ||||
| <body> | ||||
|     <div id="layout" style="display:flex; min-height:100vh;"> | ||||
|         {{if not .HideSidebar}} | ||||
|         <nav id="sidebar" style="width:220px; background:#f5f5f5; padding:2em 1.5em 2em 2em; border:1px solid #eee;"> | ||||
|             <h1 style="margin-bottom:1em;"><a href="/" id="title">Micro</a></h1> | ||||
|             {{if .User}} | ||||
|               <div style="margin-bottom:1.5em; font-size:1.05em;"> | ||||
|                 <span style="color:#888;">Logged in as</span> | ||||
|                 <b>{{.User.ID}}</b> | ||||
|                 <form method="POST" action="/auth/logout" style="margin-top:0.7em; display:block; background:none; box-shadow:none; padding:0; border:none;"> | ||||
|                   <button type="submit" style="padding:0.25em 0.8em; font-size:0.97em; border-radius:4px; margin:0; cursor:pointer;">Logout</button> | ||||
|                 </form> | ||||
|               </div> | ||||
|             {{else}} | ||||
|               <div style="margin-bottom:1.5em;"> | ||||
|                 <a href="/auth/login" class="micro-link">Login</a> | ||||
|               </div> | ||||
|             {{end}} | ||||
|             <ul class="no-bullets" style="padding-left:0;"> | ||||
|                 <li><a href="/" class="micro-link">Home</a></li> | ||||
|                 <li><a href="/services" class="micro-link">Services</a></li> | ||||
|                 <li><a href="/logs" class="micro-link">Logs</a></li> | ||||
|                 <li><a href="/status" class="micro-link">Status</a></li> | ||||
|                 <li><a href="/api" class="micro-link">API</a></li> | ||||
|                 <li><a href="/auth/tokens" class="micro-link">Tokens</a></li> | ||||
|                 <li><a href="/auth/users" class="micro-link">Users</a></li> | ||||
|             </ul> | ||||
|             {{if and .SidebarEndpoints .SidebarEndpointsEnabled}} | ||||
|             <hr style="margin:2em 0 1em 0;"> | ||||
|             <div style="font-weight:bold; margin-bottom:0.5em;">API Endpoints</div> | ||||
|             <div style="max-height:40vh; overflow-y:auto; font-size:0.97em;"> | ||||
|               {{range .SidebarEndpoints}} | ||||
|                 <div style="margin-bottom:0.3em;"><a href="#{{.Anchor}}" class="micro-link">{{.Name}}</a></div> | ||||
|               {{end}} | ||||
|             </div> | ||||
|             {{end}} | ||||
|         </nav> | ||||
|         {{end}} | ||||
|         <main class="container" style="flex:1; min-width:0;"> | ||||
|             {{template "content" .}} | ||||
|         </main> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										23
									
								
								cmd/micro/web/templates/form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								cmd/micro/web/templates/form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| {{define "content"}} | ||||
| <h2>{{.ServiceName}}</h2> | ||||
| <form action="/{{.Action}}" method="POST" data-reactive> | ||||
|     <h3 class="text-lg font-bold mb-2">{{.EndpointName}}</h3> | ||||
|     {{range .Inputs}} | ||||
|         <label class="block font-semibold">{{.Label}}</label> | ||||
|         <input name="{{.Name}}" placeholder="{{.Placeholder}}" class="border rounded px-2 py-1 mb-2 w-full" value="{{.Value}}"> | ||||
|     {{end}} | ||||
|     <button class="micro-link mt-2" type="submit">Submit</button> | ||||
|     <div class="js-response"></div> | ||||
| </form> | ||||
| {{if .Error}} | ||||
|     <div class="mt-4 text-red-600 font-bold">Error: {{.Error}}</div> | ||||
| {{end}} | ||||
| {{if .Response}} | ||||
|     <div class="mt-4"> | ||||
|         <h4 class="font-bold mb-2">Response</h4> | ||||
|         {{.ResponseTable}} | ||||
|         <pre class="bg-gray-100 rounded p-2 mt-2">{{.ResponseJSON}}</pre> | ||||
|     </div> | ||||
| {{end}} | ||||
| <script src="/main.js"></script> | ||||
| {{end}} | ||||
							
								
								
									
										21
									
								
								cmd/micro/web/templates/home.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								cmd/micro/web/templates/home.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Dashboard</h2> | ||||
| <div style="display:flex; align-items:center; gap:2em; margin-bottom:2em;"> | ||||
|   <div style="display:flex; align-items:center; gap:0.5em;"> | ||||
|     <span style="font-size:2.2em; vertical-align:middle;"> | ||||
|       {{if eq .StatusDot "green"}} | ||||
|         <span style="display:inline-block; width:1em; height:1em; background:#2ecc40; border-radius:50%;"></span> | ||||
|       {{else if eq .StatusDot "yellow"}} | ||||
|         <span style="display:inline-block; width:1em; height:1em; background:#ffcc00; border-radius:50%;"></span> | ||||
|       {{else}} | ||||
|         <span style="display:inline-block; width:1em; height:1em; background:#ff4136; border-radius:50%;"></span> | ||||
|       {{end}} | ||||
|     </span> | ||||
|     <span style="font-size:1.2em; font-weight:bold;">Status</span> | ||||
|   </div> | ||||
|   <div style="font-size:1.1em;">Services: <b>{{.ServiceCount}}</b></div> | ||||
|   <div style="font-size:1.1em; color:#2ecc40;">Running: <b>{{.RunningCount}}</b></div> | ||||
|   <div style="font-size:1.1em; color:#ff4136;">Stopped: <b>{{.StoppedCount}}</b></div> | ||||
| </div> | ||||
| <p>Welcome to the Micro dashboard. Use the sidebar to navigate services, logs, status, and API.</p> | ||||
| {{end}} | ||||
							
								
								
									
										5
									
								
								cmd/micro/web/templates/log.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								cmd/micro/web/templates/log.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Logs for {{.Service}}</h2> | ||||
| <pre class="bg-gray-100 rounded p-2 mt-2" style="max-height: 60vh; overflow-y: auto;">{{.Log}}</pre> | ||||
| <a href="/logs" class="micro-link">Back to logs</a> | ||||
| {{end}} | ||||
							
								
								
									
										8
									
								
								cmd/micro/web/templates/logs.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								cmd/micro/web/templates/logs.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Logs</h2> | ||||
| <ul class="no-bullets"> | ||||
|     {{range .Services}} | ||||
|         <li><a href="/logs/{{.}}" class="micro-link">{{.}}</a></li> | ||||
|     {{end}} | ||||
| </ul> | ||||
| {{end}} | ||||
							
								
								
									
										26
									
								
								cmd/micro/web/templates/service.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cmd/micro/web/templates/service.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| {{define "content"}} | ||||
| {{if .ServiceName}} | ||||
|   <h2 class="text-xl font-bold mb-2">{{.ServiceName}}</h2> | ||||
|   <h4 class="font-semibold mb-2">Endpoints</h4> | ||||
|   {{if .Endpoints}} | ||||
|       {{range .Endpoints}} | ||||
|       <div><a href="{{.Path}}" class="micro-link">{{.Name}}</a></div> | ||||
|       {{end}} | ||||
|   {{else}} | ||||
|       <p>No endpoints registered</p> | ||||
|   {{end}} | ||||
|   <h4 class="font-semibold mt-4 mb-2">Description</h4> | ||||
|   <pre class="bg-gray-100 rounded p-2">{{.Description}}</pre> | ||||
| {{else}} | ||||
|   <h2 class="text-2xl font-bold mb-4">Services</h2> | ||||
|   {{if .Services}} | ||||
|       <ul class="no-bullets"> | ||||
|       {{range .Services}} | ||||
|           <li><a href="/{{.}}" class="micro-link">{{.}}</a></li> | ||||
|       {{end}} | ||||
|       </ul> | ||||
|   {{else}} | ||||
|       <p>No services registered</p> | ||||
|   {{end}} | ||||
| {{end}} | ||||
| {{end}} | ||||
							
								
								
									
										29
									
								
								cmd/micro/web/templates/status.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								cmd/micro/web/templates/status.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| {{define "content"}} | ||||
| <h2 class="text-2xl font-bold mb-4">Service Status</h2> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Service</th> | ||||
|       <th>Directory</th> | ||||
|       <th>Status</th> | ||||
|       <th>PID</th> | ||||
|       <th>Uptime</th> | ||||
|       <th>ID</th> | ||||
|       <th>Logs</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     {{range .Statuses}} | ||||
|     <tr> | ||||
|       <td>{{.Service}}</td> | ||||
|       <td><code>{{.Dir}}</code></td> | ||||
|       <td>{{.Status}}</td> | ||||
|       <td>{{.PID}}</td> | ||||
|       <td>{{.Uptime}}</td> | ||||
|       <td style="font-size:0.9em; color:#888;">{{.ID}}</td> | ||||
|       <td><a href="/logs/{{.ID}}" class="log-link">View logs</a></td> | ||||
|     </tr> | ||||
|     {{end}} | ||||
|   </tbody> | ||||
| </table> | ||||
| {{end}} | ||||
							
								
								
									
										144
									
								
								cmd/protoc-gen-micro/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								cmd/protoc-gen-micro/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| # protoc-gen-micro | ||||
|  | ||||
| This is protobuf code generation for go-micro. We use protoc-gen-micro to reduce boilerplate code. | ||||
|  | ||||
| ## Install | ||||
|  | ||||
| ``` | ||||
| go install go-micro.dev/v5/cmd/protoc-gen-micro@latest | ||||
| ``` | ||||
|  | ||||
| Also required:  | ||||
|  | ||||
| - [protoc](https://github.com/google/protobuf) | ||||
| - [protoc-gen-go](https://google.golang.org/protobuf) | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Define your service as `greeter.proto` | ||||
|  | ||||
| ``` | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package greeter; | ||||
| option go_package = "/proto;greeter"; | ||||
|  | ||||
| service Greeter { | ||||
| 	rpc Hello(Request) returns (Response) {} | ||||
| } | ||||
|  | ||||
| message Request { | ||||
| 	string name = 1; | ||||
| } | ||||
|  | ||||
| message Response { | ||||
| 	string msg = 1; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| Generate the code | ||||
|  | ||||
| ``` | ||||
| protoc --proto_path=. --micro_out=. --go_out=. greeter.proto | ||||
| ``` | ||||
|  | ||||
| Your output result should be: | ||||
|  | ||||
| ``` | ||||
| ./ | ||||
|     greeter.proto	# original protobuf file | ||||
|     greeter.pb.go	# auto-generated by protoc-gen-go | ||||
|     greeter.micro.go	# auto-generated by protoc-gen-micro | ||||
| ``` | ||||
|  | ||||
| The micro generated code includes clients and handlers which reduce boiler plate code | ||||
|  | ||||
| ### Server | ||||
|  | ||||
| Register the handler with your micro server | ||||
|  | ||||
| ```go | ||||
| type Greeter struct{} | ||||
|  | ||||
| func (g *Greeter) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error { | ||||
| 	rsp.Msg = "Hello " + req.Name | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| proto.RegisterGreeterHandler(service.Server(), &Greeter{}) | ||||
| ``` | ||||
|  | ||||
| ### Client | ||||
|  | ||||
| Create a service client with your micro client | ||||
|  | ||||
| ```go | ||||
| client := proto.NewGreeterService("greeter", service.Client()) | ||||
| ``` | ||||
|  | ||||
| ### Errors | ||||
|  | ||||
| If you see an error about `protoc-gen-micro` not being found or executable, it's likely your environment may not be configured correctly. If you've already installed `protoc`, `protoc-gen-go`, and `protoc-gen-micro` ensure you've included `$GOPATH/bin` in your `PATH`. | ||||
|  | ||||
| Alternative specify the Go plugin paths as arguments to the `protoc` command | ||||
|  | ||||
| ``` | ||||
| protoc --plugin=protoc-gen-go=$GOPATH/bin/protoc-gen-go --plugin=protoc-gen-micro=$GOPATH/bin/protoc-gen-micro --proto_path=. --micro_out=. --go_out=. greeter.proto | ||||
| ``` | ||||
|  | ||||
| ### Endpoint | ||||
|  | ||||
| Add a micro API endpoint which routes directly to an RPC method | ||||
|  | ||||
| Usage: | ||||
|  | ||||
| 1. Clone `github.com/googleapis/googleapis` to use this feature as it requires http annotations. | ||||
| 2. The protoc command must include `-I$GOPATH/src/github.com/googleapis/googleapis` for the annotations import. | ||||
|  | ||||
| ```diff | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package greeter; | ||||
| option go_package = "/proto;greeter"; | ||||
|  | ||||
| import "google/api/annotations.proto"; | ||||
|  | ||||
| service Greeter { | ||||
| 	rpc Hello(Request) returns (Response) { | ||||
| 		option (google.api.http) = { post: "/hello"; body: "*"; }; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| message Request { | ||||
| 	string name = 1; | ||||
| } | ||||
|  | ||||
| message Response { | ||||
| 	string msg = 1; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| The proto generates a `RegisterGreeterHandler` function with a [api.Endpoint](https://godoc.org/go-micro.dev/v3/api#Endpoint).  | ||||
|  | ||||
| ```diff | ||||
| func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error { | ||||
| 	type greeter interface { | ||||
| 		Hello(ctx context.Context, in *Request, out *Response) error | ||||
| 	} | ||||
| 	type Greeter struct { | ||||
| 		greeter | ||||
| 	} | ||||
| 	h := &greeterHandler{hdlr} | ||||
| 	opts = append(opts, api.WithEndpoint(&api.Endpoint{ | ||||
| 		Name:    "Greeter.Hello", | ||||
| 		Path:    []string{"/hello"}, | ||||
| 		Method:  []string{"POST"}, | ||||
| 		Handler: "rpc", | ||||
| 	})) | ||||
| 	return s.Handle(s.NewHandler(&Greeter{h}, opts...)) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## LICENSE | ||||
|  | ||||
| protoc-gen-micro is a liberal reuse of protoc-gen-go hence we maintain the original license  | ||||
							
								
								
									
										223
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.pb.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| // Code generated by protoc-gen-go. DO NOT EDIT. | ||||
| // versions: | ||||
| // 	protoc-gen-go v1.32.0 | ||||
| // 	protoc        v4.25.3 | ||||
| // source: greeter.proto | ||||
|  | ||||
| package greeter | ||||
|  | ||||
| import ( | ||||
| 	protoreflect "google.golang.org/protobuf/reflect/protoreflect" | ||||
| 	protoimpl "google.golang.org/protobuf/runtime/protoimpl" | ||||
| 	reflect "reflect" | ||||
| 	sync "sync" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// Verify that this generated code is sufficiently up-to-date. | ||||
| 	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) | ||||
| 	// Verify that runtime/protoimpl is sufficiently up-to-date. | ||||
| 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) | ||||
| ) | ||||
|  | ||||
| type Request struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	Name string  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` | ||||
| 	Msg  *string `protobuf:"bytes,2,opt,name=msg,proto3,oneof" json:"msg,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Request) Reset() { | ||||
| 	*x = Request{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_greeter_proto_msgTypes[0] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Request) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Request) ProtoMessage() {} | ||||
|  | ||||
| func (x *Request) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_greeter_proto_msgTypes[0] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Request.ProtoReflect.Descriptor instead. | ||||
| func (*Request) Descriptor() ([]byte, []int) { | ||||
| 	return file_greeter_proto_rawDescGZIP(), []int{0} | ||||
| } | ||||
|  | ||||
| func (x *Request) GetName() string { | ||||
| 	if x != nil { | ||||
| 		return x.Name | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Request) GetMsg() string { | ||||
| 	if x != nil && x.Msg != nil { | ||||
| 		return *x.Msg | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| type Response struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Response) Reset() { | ||||
| 	*x = Response{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_greeter_proto_msgTypes[1] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Response) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Response) ProtoMessage() {} | ||||
|  | ||||
| func (x *Response) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_greeter_proto_msgTypes[1] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Response.ProtoReflect.Descriptor instead. | ||||
| func (*Response) Descriptor() ([]byte, []int) { | ||||
| 	return file_greeter_proto_rawDescGZIP(), []int{1} | ||||
| } | ||||
|  | ||||
| func (x *Response) GetMsg() string { | ||||
| 	if x != nil { | ||||
| 		return x.Msg | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| var File_greeter_proto protoreflect.FileDescriptor | ||||
|  | ||||
| var file_greeter_proto_rawDesc = []byte{ | ||||
| 	0x0a, 0x0d, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, | ||||
| 	0x3c, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, | ||||
| 	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, | ||||
| 	0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x6d, | ||||
| 	0x73, 0x67, 0x88, 0x01, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x6d, 0x73, 0x67, 0x22, 0x1c, 0x0a, | ||||
| 	0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, | ||||
| 	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x32, 0x4e, 0x0a, 0x07, 0x47, | ||||
| 	0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, | ||||
| 	0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, | ||||
| 	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x23, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, | ||||
| 	0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, | ||||
| 	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, | ||||
| 	0x2e, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, | ||||
| 	0x33, | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	file_greeter_proto_rawDescOnce sync.Once | ||||
| 	file_greeter_proto_rawDescData = file_greeter_proto_rawDesc | ||||
| ) | ||||
|  | ||||
| func file_greeter_proto_rawDescGZIP() []byte { | ||||
| 	file_greeter_proto_rawDescOnce.Do(func() { | ||||
| 		file_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(file_greeter_proto_rawDescData) | ||||
| 	}) | ||||
| 	return file_greeter_proto_rawDescData | ||||
| } | ||||
|  | ||||
| var file_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2) | ||||
| var file_greeter_proto_goTypes = []interface{}{ | ||||
| 	(*Request)(nil),  // 0: Request | ||||
| 	(*Response)(nil), // 1: Response | ||||
| } | ||||
| var file_greeter_proto_depIdxs = []int32{ | ||||
| 	0, // 0: Greeter.Hello:input_type -> Request | ||||
| 	0, // 1: Greeter.Stream:input_type -> Request | ||||
| 	1, // 2: Greeter.Hello:output_type -> Response | ||||
| 	1, // 3: Greeter.Stream:output_type -> Response | ||||
| 	2, // [2:4] is the sub-list for method output_type | ||||
| 	0, // [0:2] is the sub-list for method input_type | ||||
| 	0, // [0:0] is the sub-list for extension type_name | ||||
| 	0, // [0:0] is the sub-list for extension extendee | ||||
| 	0, // [0:0] is the sub-list for field type_name | ||||
| } | ||||
|  | ||||
| func init() { file_greeter_proto_init() } | ||||
| func file_greeter_proto_init() { | ||||
| 	if File_greeter_proto != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if !protoimpl.UnsafeEnabled { | ||||
| 		file_greeter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Request); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		file_greeter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Response); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	file_greeter_proto_msgTypes[0].OneofWrappers = []interface{}{} | ||||
| 	type x struct{} | ||||
| 	out := protoimpl.TypeBuilder{ | ||||
| 		File: protoimpl.DescBuilder{ | ||||
| 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | ||||
| 			RawDescriptor: file_greeter_proto_rawDesc, | ||||
| 			NumEnums:      0, | ||||
| 			NumMessages:   2, | ||||
| 			NumExtensions: 0, | ||||
| 			NumServices:   1, | ||||
| 		}, | ||||
| 		GoTypes:           file_greeter_proto_goTypes, | ||||
| 		DependencyIndexes: file_greeter_proto_depIdxs, | ||||
| 		MessageInfos:      file_greeter_proto_msgTypes, | ||||
| 	}.Build() | ||||
| 	File_greeter_proto = out.File | ||||
| 	file_greeter_proto_rawDesc = nil | ||||
| 	file_greeter_proto_goTypes = nil | ||||
| 	file_greeter_proto_depIdxs = nil | ||||
| } | ||||
							
								
								
									
										183
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.pb.micro.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||
| // source: greeter.proto | ||||
|  | ||||
| package greeter | ||||
|  | ||||
| import ( | ||||
| 	fmt "fmt" | ||||
| 	proto "google.golang.org/protobuf/proto" | ||||
| 	math "math" | ||||
| ) | ||||
|  | ||||
| import ( | ||||
| 	context "context" | ||||
| 	client "go-micro.dev/v5/client" | ||||
| 	server "go-micro.dev/v5/server" | ||||
| ) | ||||
|  | ||||
| // Reference imports to suppress errors if they are not otherwise used. | ||||
| var _ = proto.Marshal | ||||
| var _ = fmt.Errorf | ||||
| var _ = math.Inf | ||||
|  | ||||
| // Reference imports to suppress errors if they are not otherwise used. | ||||
| var _ context.Context | ||||
| var _ client.Option | ||||
| var _ server.Option | ||||
|  | ||||
| // Client API for Greeter service | ||||
|  | ||||
| type GreeterService interface { | ||||
| 	Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) | ||||
| 	Stream(ctx context.Context, opts ...client.CallOption) (Greeter_StreamService, error) | ||||
| } | ||||
|  | ||||
| type greeterService struct { | ||||
| 	c    client.Client | ||||
| 	name string | ||||
| } | ||||
|  | ||||
| func NewGreeterService(name string, c client.Client) GreeterService { | ||||
| 	return &greeterService{ | ||||
| 		c:    c, | ||||
| 		name: name, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *greeterService) Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) { | ||||
| 	req := c.c.NewRequest(c.name, "Greeter.Hello", in) | ||||
| 	out := new(Response) | ||||
| 	err := c.c.Call(ctx, req, out, opts...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return out, nil | ||||
| } | ||||
|  | ||||
| func (c *greeterService) Stream(ctx context.Context, opts ...client.CallOption) (Greeter_StreamService, error) { | ||||
| 	req := c.c.NewRequest(c.name, "Greeter.Stream", &Request{}) | ||||
| 	stream, err := c.c.Stream(ctx, req, opts...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &greeterServiceStream{stream}, nil | ||||
| } | ||||
|  | ||||
| type Greeter_StreamService interface { | ||||
| 	Context() context.Context | ||||
| 	SendMsg(interface{}) error | ||||
| 	RecvMsg(interface{}) error | ||||
| 	CloseSend() error | ||||
| 	Close() error | ||||
| 	Send(*Request) error | ||||
| 	Recv() (*Response, error) | ||||
| } | ||||
|  | ||||
| type greeterServiceStream struct { | ||||
| 	stream client.Stream | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) CloseSend() error { | ||||
| 	return x.stream.CloseSend() | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) Close() error { | ||||
| 	return x.stream.Close() | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) Context() context.Context { | ||||
| 	return x.stream.Context() | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) SendMsg(m interface{}) error { | ||||
| 	return x.stream.Send(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) RecvMsg(m interface{}) error { | ||||
| 	return x.stream.Recv(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) Send(m *Request) error { | ||||
| 	return x.stream.Send(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterServiceStream) Recv() (*Response, error) { | ||||
| 	m := new(Response) | ||||
| 	err := x.stream.Recv(m) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return m, nil | ||||
| } | ||||
|  | ||||
| // Server API for Greeter service | ||||
|  | ||||
| type GreeterHandler interface { | ||||
| 	Hello(context.Context, *Request, *Response) error | ||||
| 	Stream(context.Context, Greeter_StreamStream) error | ||||
| } | ||||
|  | ||||
| func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error { | ||||
| 	type greeter interface { | ||||
| 		Hello(ctx context.Context, in *Request, out *Response) error | ||||
| 		Stream(ctx context.Context, stream server.Stream) error | ||||
| 	} | ||||
| 	type Greeter struct { | ||||
| 		greeter | ||||
| 	} | ||||
| 	h := &greeterHandler{hdlr} | ||||
| 	return s.Handle(s.NewHandler(&Greeter{h}, opts...)) | ||||
| } | ||||
|  | ||||
| type greeterHandler struct { | ||||
| 	GreeterHandler | ||||
| } | ||||
|  | ||||
| func (h *greeterHandler) Hello(ctx context.Context, in *Request, out *Response) error { | ||||
| 	return h.GreeterHandler.Hello(ctx, in, out) | ||||
| } | ||||
|  | ||||
| func (h *greeterHandler) Stream(ctx context.Context, stream server.Stream) error { | ||||
| 	return h.GreeterHandler.Stream(ctx, &greeterStreamStream{stream}) | ||||
| } | ||||
|  | ||||
| type Greeter_StreamStream interface { | ||||
| 	Context() context.Context | ||||
| 	SendMsg(interface{}) error | ||||
| 	RecvMsg(interface{}) error | ||||
| 	Close() error | ||||
| 	Send(*Response) error | ||||
| 	Recv() (*Request, error) | ||||
| } | ||||
|  | ||||
| type greeterStreamStream struct { | ||||
| 	stream server.Stream | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) Close() error { | ||||
| 	return x.stream.Close() | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) Context() context.Context { | ||||
| 	return x.stream.Context() | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) SendMsg(m interface{}) error { | ||||
| 	return x.stream.Send(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) RecvMsg(m interface{}) error { | ||||
| 	return x.stream.Recv(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) Send(m *Response) error { | ||||
| 	return x.stream.Send(m) | ||||
| } | ||||
|  | ||||
| func (x *greeterStreamStream) Recv() (*Request, error) { | ||||
| 	m := new(Request) | ||||
| 	if err := x.stream.Recv(m); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return m, nil | ||||
| } | ||||
							
								
								
									
										17
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cmd/protoc-gen-micro/examples/greeter/greeter.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| option go_package = "../greeter"; | ||||
|  | ||||
| service Greeter { | ||||
| 	rpc Hello(Request) returns (Response) {} | ||||
| 	rpc Stream(stream Request) returns (stream Response) {} | ||||
| } | ||||
|  | ||||
| message Request { | ||||
| 	string name = 1; | ||||
| 	optional string msg = 2; | ||||
| } | ||||
|  | ||||
| message Response { | ||||
| 	string msg = 1; | ||||
| } | ||||
							
								
								
									
										40
									
								
								cmd/protoc-gen-micro/generator/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								cmd/protoc-gen-micro/generator/Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| # Go support for Protocol Buffers - Google's data interchange format | ||||
| # | ||||
| # Copyright 2010 The Go Authors.  All rights reserved. | ||||
| # https://github.com/golang/protobuf | ||||
| # | ||||
| # Redistribution and use in source and binary forms, with or without | ||||
| # modification, are permitted provided that the following conditions are | ||||
| # met: | ||||
| # | ||||
| #     * Redistributions of source code must retain the above copyright | ||||
| # notice, this list of conditions and the following disclaimer. | ||||
| #     * Redistributions in binary form must reproduce the above | ||||
| # copyright notice, this list of conditions and the following disclaimer | ||||
| # in the documentation and/or other materials provided with the | ||||
| # distribution. | ||||
| #     * Neither the name of Google Inc. nor the names of its | ||||
| # contributors may be used to endorse or promote products derived from | ||||
| # this software without specific prior written permission. | ||||
| # | ||||
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
| include $(GOROOT)/src/Make.inc | ||||
|  | ||||
| TARG=github.com/golang/protobuf/protoc-gen-go/generator | ||||
| GOFILES=\ | ||||
| 	generator.go\ | ||||
|  | ||||
| DEPS=../descriptor ../plugin ../../proto | ||||
|  | ||||
| include $(GOROOT)/src/Make.pkg | ||||
							
								
								
									
										2748
									
								
								cmd/protoc-gen-micro/generator/generator.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2748
									
								
								cmd/protoc-gen-micro/generator/generator.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										135
									
								
								cmd/protoc-gen-micro/generator/name_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								cmd/protoc-gen-micro/generator/name_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| // Go support for Protocol Buffers - Google's data interchange format | ||||
| // | ||||
| // Copyright 2013 The Go Authors.  All rights reserved. | ||||
| // https://github.com/golang/protobuf | ||||
| // | ||||
| // Redistribution and use in source and binary forms, with or without | ||||
| // modification, are permitted provided that the following conditions are | ||||
| // met: | ||||
| // | ||||
| //     * Redistributions of source code must retain the above copyright | ||||
| // notice, this list of conditions and the following disclaimer. | ||||
| //     * Redistributions in binary form must reproduce the above | ||||
| // copyright notice, this list of conditions and the following disclaimer | ||||
| // in the documentation and/or other materials provided with the | ||||
| // distribution. | ||||
| //     * Neither the name of Google Inc. nor the names of its | ||||
| // contributors may be used to endorse or promote products derived from | ||||
| // this software without specific prior written permission. | ||||
| // | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
| package generator | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	descriptor "google.golang.org/protobuf/types/descriptorpb" | ||||
| ) | ||||
|  | ||||
| func TestCamelCase(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		in, want string | ||||
| 	}{ | ||||
| 		{"one", "One"}, | ||||
| 		{"one_two", "OneTwo"}, | ||||
| 		{"_my_field_name_2", "XMyFieldName_2"}, | ||||
| 		{"Something_Capped", "Something_Capped"}, | ||||
| 		{"my_Name", "My_Name"}, | ||||
| 		{"OneTwo", "OneTwo"}, | ||||
| 		{"_", "X"}, | ||||
| 		{"_a_", "XA_"}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		if got := CamelCase(tc.in); got != tc.want { | ||||
| 			t.Errorf("CamelCase(%q) = %q, want %q", tc.in, got, tc.want) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGoPackageOption(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		in      string | ||||
| 		impPath GoImportPath | ||||
| 		pkg     GoPackageName | ||||
| 		ok      bool | ||||
| 	}{ | ||||
| 		{"", "", "", false}, | ||||
| 		{"foo", "", "foo", true}, | ||||
| 		{"github.com/golang/bar", "github.com/golang/bar", "bar", true}, | ||||
| 		{"github.com/golang/bar;baz", "github.com/golang/bar", "baz", true}, | ||||
| 		{"github.com/golang/string", "github.com/golang/string", "string", true}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		d := &FileDescriptor{ | ||||
| 			FileDescriptorProto: &descriptor.FileDescriptorProto{ | ||||
| 				Options: &descriptor.FileOptions{ | ||||
| 					GoPackage: &tc.in, | ||||
| 				}, | ||||
| 			}, | ||||
| 		} | ||||
| 		impPath, pkg, ok := d.goPackageOption() | ||||
| 		if impPath != tc.impPath || pkg != tc.pkg || ok != tc.ok { | ||||
| 			t.Errorf("go_package = %q => (%q, %q, %t), want (%q, %q, %t)", tc.in, | ||||
| 				impPath, pkg, ok, tc.impPath, tc.pkg, tc.ok) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPackageNames(t *testing.T) { | ||||
| 	g := New() | ||||
| 	g.packageNames = make(map[GoImportPath]GoPackageName) | ||||
| 	g.usedPackageNames = make(map[GoPackageName]bool) | ||||
| 	for _, test := range []struct { | ||||
| 		importPath GoImportPath | ||||
| 		want       GoPackageName | ||||
| 	}{ | ||||
| 		{"github.com/golang/foo", "foo"}, | ||||
| 		{"github.com/golang/second/package/named/foo", "foo1"}, | ||||
| 		{"github.com/golang/third/package/named/foo", "foo2"}, | ||||
| 		{"github.com/golang/conflicts/with/predeclared/ident/string", "string1"}, | ||||
| 	} { | ||||
| 		if got := g.GoPackageName(test.importPath); got != test.want { | ||||
| 			t.Errorf("GoPackageName(%v) = %v, want %v", test.importPath, got, test.want) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestUnescape(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		in  string | ||||
| 		out string | ||||
| 	}{ | ||||
| 		// successful cases, including all kinds of escapes | ||||
| 		{"", ""}, | ||||
| 		{"foo bar baz frob nitz", "foo bar baz frob nitz"}, | ||||
| 		{`\000\001\002\003\004\005\006\007`, string([]byte{0, 1, 2, 3, 4, 5, 6, 7})}, | ||||
| 		{`\a\b\f\n\r\t\v\\\?\'\"`, string([]byte{'\a', '\b', '\f', '\n', '\r', '\t', '\v', '\\', '?', '\'', '"'})}, | ||||
| 		{`\x10\x20\x30\x40\x50\x60\x70\x80`, string([]byte{16, 32, 48, 64, 80, 96, 112, 128})}, | ||||
| 		// variable length octal escapes | ||||
| 		{`\0\018\222\377\3\04\005\6\07`, string([]byte{0, 1, '8', 0222, 255, 3, 4, 5, 6, 7})}, | ||||
| 		// malformed escape sequences left as is | ||||
| 		{"foo \\g bar", "foo \\g bar"}, | ||||
| 		{"foo \\xg0 bar", "foo \\xg0 bar"}, | ||||
| 		{"\\", "\\"}, | ||||
| 		{"\\x", "\\x"}, | ||||
| 		{"\\xf", "\\xf"}, | ||||
| 		{"\\777", "\\777"}, // overflows byte | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		s := unescape(tc.in) | ||||
| 		if s != tc.out { | ||||
| 			t.Errorf("doUnescape(%q) = %q; should have been %q", tc.in, s, tc.out) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										105
									
								
								cmd/protoc-gen-micro/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								cmd/protoc-gen-micro/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| // Go support for Protocol Buffers - Google's data interchange format | ||||
| // | ||||
| // Copyright 2010 The Go Authors.  All rights reserved. | ||||
| // https://github.com/golang/protobuf | ||||
| // | ||||
| // Redistribution and use in source and binary forms, with or without | ||||
| // modification, are permitted provided that the following conditions are | ||||
| // met: | ||||
| // | ||||
| //     * Redistributions of source code must retain the above copyright | ||||
| // notice, this list of conditions and the following disclaimer. | ||||
| //     * Redistributions in binary form must reproduce the above | ||||
| // copyright notice, this list of conditions and the following disclaimer | ||||
| // in the documentation and/or other materials provided with the | ||||
| // distribution. | ||||
| //     * Neither the name of Google Inc. nor the names of its | ||||
| // contributors may be used to endorse or promote products derived from | ||||
| // this software without specific prior written permission. | ||||
| // | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
| // protoc-gen-micro is a plugin for the Google protocol buffer compiler to generate | ||||
| // Go code.  Run it by building this program and putting it in your path with | ||||
| // the name | ||||
| // | ||||
| //	protoc-gen-micro | ||||
| // | ||||
| // That word 'micro' at the end becomes part of the option string set for the | ||||
| // protocol compiler, so once the protocol compiler (protoc) is installed | ||||
| // you can run | ||||
| // | ||||
| //	protoc --micro_out=output_directory --go_out=output_directory input_directory/file.proto | ||||
| // | ||||
| // to generate go-micro code for the protocol defined by file.proto. | ||||
| // With that input, the output will be written to | ||||
| // | ||||
| //	output_directory/file.micro.go | ||||
| // | ||||
| // The generated code is documented in the package comment for | ||||
| // the library. | ||||
| // | ||||
| // See the README and documentation for protocol buffers to learn more: | ||||
| // | ||||
| //	https://developers.google.com/protocol-buffers/ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"os" | ||||
|  | ||||
| 	"go-micro.dev/v5/cmd/protoc-gen-micro/generator" | ||||
| 	_ "go-micro.dev/v5/cmd/protoc-gen-micro/plugin/micro" | ||||
| 	"google.golang.org/protobuf/proto" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// Begin by allocating a generator. The request and response structures are stored there | ||||
| 	// so we can do error handling easily - the response structure contains the field to | ||||
| 	// report failure. | ||||
| 	g := generator.New() | ||||
|  | ||||
| 	data, err := io.ReadAll(os.Stdin) | ||||
| 	if err != nil { | ||||
| 		g.Error(err, "reading input") | ||||
| 	} | ||||
|  | ||||
| 	if err := proto.Unmarshal(data, g.Request); err != nil { | ||||
| 		g.Error(err, "parsing input proto") | ||||
| 	} | ||||
|  | ||||
| 	if len(g.Request.FileToGenerate) == 0 { | ||||
| 		g.Fail("no files to generate") | ||||
| 	} | ||||
|  | ||||
| 	g.CommandLineParameters(g.Request.GetParameter()) | ||||
|  | ||||
| 	// Create a wrapped version of the Descriptors and EnumDescriptors that | ||||
| 	// point to the file that defines them. | ||||
| 	g.WrapTypes() | ||||
|  | ||||
| 	g.SetPackageNames() | ||||
| 	g.BuildTypeNameMap() | ||||
|  | ||||
| 	g.GenerateAllFiles() | ||||
|  | ||||
| 	// Send back the results. | ||||
| 	data, err = proto.Marshal(g.Response) | ||||
| 	if err != nil { | ||||
| 		g.Error(err, "failed to marshal output proto") | ||||
| 	} | ||||
| 	_, err = os.Stdout.Write(data) | ||||
| 	if err != nil { | ||||
| 		g.Error(err, "failed to write output proto") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										531
									
								
								cmd/protoc-gen-micro/plugin/micro/micro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										531
									
								
								cmd/protoc-gen-micro/plugin/micro/micro.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,531 @@ | ||||
| package micro | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"go-micro.dev/v5/cmd/protoc-gen-micro/generator" | ||||
| 	options "google.golang.org/genproto/googleapis/api/annotations" | ||||
| 	"google.golang.org/protobuf/proto" | ||||
| 	pb "google.golang.org/protobuf/types/descriptorpb" | ||||
| ) | ||||
|  | ||||
| // Paths for packages used by code generated in this file, | ||||
| // relative to the import_prefix of the generator.Generator. | ||||
| const ( | ||||
| 	contextPkgPath = "context" | ||||
| 	clientPkgPath  = "go-micro.dev/v5/client" | ||||
| 	serverPkgPath  = "go-micro.dev/v5/server" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	generator.RegisterPlugin(new(micro)) | ||||
| } | ||||
|  | ||||
| // micro is an implementation of the Go protocol buffer compiler's | ||||
| // plugin architecture.  It generates bindings for go-micro support. | ||||
| type micro struct { | ||||
| 	gen *generator.Generator | ||||
| } | ||||
|  | ||||
| // Name returns the name of this plugin, "micro". | ||||
| func (g *micro) Name() string { | ||||
| 	return "micro" | ||||
| } | ||||
|  | ||||
| // The names for packages imported in the generated code. | ||||
| // They may vary from the final path component of the import path | ||||
| // if the name is used by other packages. | ||||
| var ( | ||||
| 	contextPkg string | ||||
| 	clientPkg  string | ||||
| 	serverPkg  string | ||||
| 	pkgImports map[generator.GoPackageName]bool | ||||
| ) | ||||
|  | ||||
| // Init initializes the plugin. | ||||
| func (g *micro) Init(gen *generator.Generator) { | ||||
| 	g.gen = gen | ||||
| 	contextPkg = generator.RegisterUniquePackageName("context", nil) | ||||
| 	clientPkg = generator.RegisterUniquePackageName("client", nil) | ||||
| 	serverPkg = generator.RegisterUniquePackageName("server", nil) | ||||
| } | ||||
|  | ||||
| // Given a type name defined in a .proto, return its object. | ||||
| // Also record that we're using it, to guarantee the associated import. | ||||
| func (g *micro) objectNamed(name string) generator.Object { | ||||
| 	g.gen.RecordTypeUse(name) | ||||
| 	return g.gen.ObjectNamed(name) | ||||
| } | ||||
|  | ||||
| // Given a type name defined in a .proto, return its name as we will print it. | ||||
| func (g *micro) typeName(str string) string { | ||||
| 	return g.gen.TypeName(g.objectNamed(str)) | ||||
| } | ||||
|  | ||||
| // P forwards to g.gen.P. | ||||
| func (g *micro) P(args ...interface{}) { g.gen.P(args...) } | ||||
|  | ||||
| // Generate generates code for the services in the given file. | ||||
| func (g *micro) Generate(file *generator.FileDescriptor) { | ||||
| 	if len(file.FileDescriptorProto.Service) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	g.P("// Reference imports to suppress errors if they are not otherwise used.") | ||||
| 	g.P("var _ ", contextPkg, ".Context") | ||||
| 	g.P("var _ ", clientPkg, ".Option") | ||||
| 	g.P("var _ ", serverPkg, ".Option") | ||||
| 	g.P() | ||||
|  | ||||
| 	for i, service := range file.FileDescriptorProto.Service { | ||||
| 		g.generateService(file, service, i) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GenerateImports generates the import declaration for this file. | ||||
| func (g *micro) GenerateImports(file *generator.FileDescriptor, imports map[generator.GoImportPath]generator.GoPackageName) { | ||||
| 	if len(file.FileDescriptorProto.Service) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	g.P("import (") | ||||
| 	g.P(contextPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, contextPkgPath))) | ||||
| 	g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath))) | ||||
| 	g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath))) | ||||
| 	g.P(")") | ||||
| 	g.P() | ||||
|  | ||||
| 	// We need to keep track of imported packages to make sure we don't produce | ||||
| 	// a name collision when generating types. | ||||
| 	pkgImports = make(map[generator.GoPackageName]bool) | ||||
| 	for _, name := range imports { | ||||
| 		pkgImports[name] = true | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // reservedClientName records whether a client name is reserved on the client side. | ||||
| var reservedClientName = map[string]bool{ | ||||
| 	// TODO: do we need any in go-micro? | ||||
| } | ||||
|  | ||||
| func unexport(s string) string { | ||||
| 	if len(s) == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
| 	name := strings.ToLower(s[:1]) + s[1:] | ||||
| 	if pkgImports[generator.GoPackageName(name)] { | ||||
| 		return name + "_" | ||||
| 	} | ||||
| 	return name | ||||
| } | ||||
|  | ||||
| // generateService generates all the code for the named service. | ||||
| func (g *micro) generateService(file *generator.FileDescriptor, service *pb.ServiceDescriptorProto, index int) { | ||||
| 	path := fmt.Sprintf("6,%d", index) // 6 means service. | ||||
|  | ||||
| 	origServName := service.GetName() | ||||
| 	serviceName := strings.ToLower(service.GetName()) | ||||
| 	pkg := file.GetPackage() | ||||
| 	if pkg != "" { | ||||
| 		serviceName = pkg | ||||
| 	} | ||||
| 	servName := generator.CamelCase(origServName) | ||||
| 	servAlias := servName + "Service" | ||||
|  | ||||
| 	// strip suffix | ||||
| 	if strings.HasSuffix(servAlias, "ServiceService") { | ||||
| 		servAlias = strings.TrimSuffix(servAlias, "Service") | ||||
| 	} | ||||
|  | ||||
| 	g.P() | ||||
| 	g.P("// Client API for ", servName, " service") | ||||
| 	g.P() | ||||
|  | ||||
| 	// Client interface. | ||||
| 	g.P("type ", servAlias, " interface {") | ||||
| 	for i, method := range service.Method { | ||||
| 		g.gen.PrintComments(fmt.Sprintf("%s,2,%d", path, i)) // 2 means method in a service. | ||||
| 		g.P(g.generateClientSignature(servName, method)) | ||||
| 	} | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	// Client structure. | ||||
| 	g.P("type ", unexport(servAlias), " struct {") | ||||
| 	g.P("c ", clientPkg, ".Client") | ||||
| 	g.P("name string") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	// NewClient factory. | ||||
| 	g.P("func New", servAlias, " (name string, c ", clientPkg, ".Client) ", servAlias, " {") | ||||
| 	/* | ||||
| 		g.P("if c == nil {") | ||||
| 		g.P("c = ", clientPkg, ".NewClient()") | ||||
| 		g.P("}") | ||||
| 		g.P("if len(name) == 0 {") | ||||
| 		g.P(`name = "`, serviceName, `"`) | ||||
| 		g.P("}") | ||||
| 	*/ | ||||
| 	g.P("return &", unexport(servAlias), "{") | ||||
| 	g.P("c: c,") | ||||
| 	g.P("name: name,") | ||||
| 	g.P("}") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
| 	var methodIndex, streamIndex int | ||||
| 	serviceDescVar := "_" + servName + "_serviceDesc" | ||||
| 	// Client method implementations. | ||||
| 	for _, method := range service.Method { | ||||
| 		var descExpr string | ||||
| 		if !method.GetServerStreaming() { | ||||
| 			// Unary RPC method | ||||
| 			descExpr = fmt.Sprintf("&%s.Methods[%d]", serviceDescVar, methodIndex) | ||||
| 			methodIndex++ | ||||
| 		} else { | ||||
| 			// Streaming RPC method | ||||
| 			descExpr = fmt.Sprintf("&%s.Streams[%d]", serviceDescVar, streamIndex) | ||||
| 			streamIndex++ | ||||
| 		} | ||||
| 		g.generateClientMethod(pkg, serviceName, servName, serviceDescVar, method, descExpr) | ||||
| 	} | ||||
|  | ||||
| 	g.P("// Server API for ", servName, " service") | ||||
| 	g.P() | ||||
|  | ||||
| 	// Server interface. | ||||
| 	serverType := servName + "Handler" | ||||
| 	g.P("type ", serverType, " interface {") | ||||
| 	for i, method := range service.Method { | ||||
| 		g.gen.PrintComments(fmt.Sprintf("%s,2,%d", path, i)) // 2 means method in a service. | ||||
| 		g.P(g.generateServerSignature(servName, method)) | ||||
| 	} | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	// Server registration. | ||||
| 	g.P("func Register", servName, "Handler(s ", serverPkg, ".Server, hdlr ", serverType, ", opts ...", serverPkg, ".HandlerOption) error {") | ||||
| 	g.P("type ", unexport(servName), " interface {") | ||||
|  | ||||
| 	// generate interface methods | ||||
| 	for _, method := range service.Method { | ||||
| 		methName := generator.CamelCase(method.GetName()) | ||||
| 		inType := g.typeName(method.GetInputType()) | ||||
| 		outType := g.typeName(method.GetOutputType()) | ||||
|  | ||||
| 		if !method.GetServerStreaming() && !method.GetClientStreaming() { | ||||
| 			g.P(methName, "(ctx ", contextPkg, ".Context, in *", inType, ", out *", outType, ") error") | ||||
| 			continue | ||||
| 		} | ||||
| 		g.P(methName, "(ctx ", contextPkg, ".Context, stream server.Stream) error") | ||||
| 	} | ||||
| 	g.P("}") | ||||
| 	g.P("type ", servName, " struct {") | ||||
| 	g.P(unexport(servName)) | ||||
| 	g.P("}") | ||||
| 	g.P("h := &", unexport(servName), "Handler{hdlr}") | ||||
| 	g.P("return s.Handle(s.NewHandler(&", servName, "{h}, opts...))") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("type ", unexport(servName), "Handler struct {") | ||||
| 	g.P(serverType) | ||||
| 	g.P("}") | ||||
|  | ||||
| 	// Server handler implementations. | ||||
| 	var handlerNames []string | ||||
| 	for _, method := range service.Method { | ||||
| 		hname := g.generateServerMethod(servName, method) | ||||
| 		handlerNames = append(handlerNames, hname) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // generateEndpoint creates the api endpoint | ||||
| func (g *micro) generateEndpoint(servName string, method *pb.MethodDescriptorProto) { | ||||
| 	if method.Options == nil || !proto.HasExtension(method.Options, options.E_Http) { | ||||
| 		return | ||||
| 	} | ||||
| 	// http rules | ||||
| 	r := proto.GetExtension(method.Options, options.E_Http) | ||||
| 	rule := r.(*options.HttpRule) | ||||
| 	var meth string | ||||
| 	var path string | ||||
| 	switch { | ||||
| 	case len(rule.GetDelete()) > 0: | ||||
| 		meth = "DELETE" | ||||
| 		path = rule.GetDelete() | ||||
| 	case len(rule.GetGet()) > 0: | ||||
| 		meth = "GET" | ||||
| 		path = rule.GetGet() | ||||
| 	case len(rule.GetPatch()) > 0: | ||||
| 		meth = "PATCH" | ||||
| 		path = rule.GetPatch() | ||||
| 	case len(rule.GetPost()) > 0: | ||||
| 		meth = "POST" | ||||
| 		path = rule.GetPost() | ||||
| 	case len(rule.GetPut()) > 0: | ||||
| 		meth = "PUT" | ||||
| 		path = rule.GetPut() | ||||
| 	} | ||||
| 	if len(meth) == 0 || len(path) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	// TODO: process additional bindings | ||||
| 	g.P("Name:", fmt.Sprintf(`"%s.%s",`, servName, method.GetName())) | ||||
| 	g.P("Path:", fmt.Sprintf(`[]string{"%s"},`, path)) | ||||
| 	g.P("Method:", fmt.Sprintf(`[]string{"%s"},`, meth)) | ||||
| 	if method.GetServerStreaming() || method.GetClientStreaming() { | ||||
| 		g.P("Stream: true,") | ||||
| 	} | ||||
| 	g.P(`Handler: "rpc",`) | ||||
| } | ||||
|  | ||||
| // generateClientSignature returns the client-side signature for a method. | ||||
| func (g *micro) generateClientSignature(servName string, method *pb.MethodDescriptorProto) string { | ||||
| 	origMethName := method.GetName() | ||||
| 	methName := generator.CamelCase(origMethName) | ||||
| 	if reservedClientName[methName] { | ||||
| 		methName += "_" | ||||
| 	} | ||||
| 	reqArg := ", in *" + g.typeName(method.GetInputType()) | ||||
| 	if method.GetClientStreaming() { | ||||
| 		reqArg = "" | ||||
| 	} | ||||
| 	respName := "*" + g.typeName(method.GetOutputType()) | ||||
| 	if method.GetServerStreaming() || method.GetClientStreaming() { | ||||
| 		respName = servName + "_" + generator.CamelCase(origMethName) + "Service" | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s(ctx %s.Context%s, opts ...%s.CallOption) (%s, error)", methName, contextPkg, reqArg, clientPkg, respName) | ||||
| } | ||||
|  | ||||
| func (g *micro) generateClientMethod(pkg, reqServ, servName, serviceDescVar string, method *pb.MethodDescriptorProto, descExpr string) { | ||||
| 	reqMethod := fmt.Sprintf("%s.%s", servName, method.GetName()) | ||||
| 	useGrpc := g.gen.Param["use_grpc"] | ||||
| 	if useGrpc != "" { | ||||
| 		reqMethod = fmt.Sprintf("/%s.%s/%s", pkg, servName, method.GetName()) | ||||
| 	} | ||||
| 	methName := generator.CamelCase(method.GetName()) | ||||
| 	inType := g.typeName(method.GetInputType()) | ||||
| 	outType := g.typeName(method.GetOutputType()) | ||||
|  | ||||
| 	servAlias := servName + "Service" | ||||
|  | ||||
| 	// strip suffix | ||||
| 	if strings.HasSuffix(servAlias, "ServiceService") { | ||||
| 		servAlias = strings.TrimSuffix(servAlias, "Service") | ||||
| 	} | ||||
|  | ||||
| 	g.P("func (c *", unexport(servAlias), ") ", g.generateClientSignature(servName, method), "{") | ||||
| 	if !method.GetServerStreaming() && !method.GetClientStreaming() { | ||||
| 		g.P(`req := c.c.NewRequest(c.name, "`, reqMethod, `", in)`) | ||||
| 		g.P("out := new(", outType, ")") | ||||
| 		// TODO: Pass descExpr to Invoke. | ||||
| 		g.P("err := ", `c.c.Call(ctx, req, out, opts...)`) | ||||
| 		g.P("if err != nil { return nil, err }") | ||||
| 		g.P("return out, nil") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
| 		return | ||||
| 	} | ||||
| 	streamType := unexport(servAlias) + methName | ||||
| 	g.P(`req := c.c.NewRequest(c.name, "`, reqMethod, `", &`, inType, `{})`) | ||||
| 	g.P("stream, err := c.c.Stream(ctx, req, opts...)") | ||||
| 	g.P("if err != nil { return nil, err }") | ||||
|  | ||||
| 	if !method.GetClientStreaming() { | ||||
| 		g.P("if err := stream.Send(in); err != nil { return nil, err }") | ||||
| 		// TODO: currently only grpc support CloseSend | ||||
| 		// g.P("if err := stream.CloseSend(); err != nil { return nil, err }") | ||||
| 	} | ||||
|  | ||||
| 	g.P("return &", streamType, "{stream}, nil") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	genSend := method.GetClientStreaming() | ||||
| 	genRecv := method.GetServerStreaming() | ||||
|  | ||||
| 	// Stream auxiliary types and methods. | ||||
| 	g.P("type ", servName, "_", methName, "Service interface {") | ||||
| 	g.P("Context() context.Context") | ||||
| 	g.P("SendMsg(interface{}) error") | ||||
| 	g.P("RecvMsg(interface{}) error") | ||||
| 	g.P("CloseSend() error") | ||||
| 	g.P("Close() error") | ||||
|  | ||||
| 	if genSend { | ||||
| 		g.P("Send(*", inType, ") error") | ||||
| 	} | ||||
| 	if genRecv { | ||||
| 		g.P("Recv() (*", outType, ", error)") | ||||
| 	} | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("type ", streamType, " struct {") | ||||
| 	g.P("stream ", clientPkg, ".Stream") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") CloseSend() error {") | ||||
| 	g.P("return x.stream.CloseSend()") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") Close() error {") | ||||
| 	g.P("return x.stream.Close()") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") Context() context.Context {") | ||||
| 	g.P("return x.stream.Context()") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") SendMsg(m interface{}) error {") | ||||
| 	g.P("return x.stream.Send(m)") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") RecvMsg(m interface{}) error {") | ||||
| 	g.P("return x.stream.Recv(m)") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	if genSend { | ||||
| 		g.P("func (x *", streamType, ") Send(m *", inType, ") error {") | ||||
| 		g.P("return x.stream.Send(m)") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if genRecv { | ||||
| 		g.P("func (x *", streamType, ") Recv() (*", outType, ", error) {") | ||||
| 		g.P("m := new(", outType, ")") | ||||
| 		g.P("err := x.stream.Recv(m)") | ||||
| 		g.P("if err != nil {") | ||||
| 		g.P("return nil, err") | ||||
| 		g.P("}") | ||||
| 		g.P("return m, nil") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // generateServerSignature returns the server-side signature for a method. | ||||
| func (g *micro) generateServerSignature(servName string, method *pb.MethodDescriptorProto) string { | ||||
| 	origMethName := method.GetName() | ||||
| 	methName := generator.CamelCase(origMethName) | ||||
| 	if reservedClientName[methName] { | ||||
| 		methName += "_" | ||||
| 	} | ||||
|  | ||||
| 	var reqArgs []string | ||||
| 	ret := "error" | ||||
| 	reqArgs = append(reqArgs, contextPkg+".Context") | ||||
|  | ||||
| 	if !method.GetClientStreaming() { | ||||
| 		reqArgs = append(reqArgs, "*"+g.typeName(method.GetInputType())) | ||||
| 	} | ||||
| 	if method.GetServerStreaming() || method.GetClientStreaming() { | ||||
| 		reqArgs = append(reqArgs, servName+"_"+generator.CamelCase(origMethName)+"Stream") | ||||
| 	} | ||||
| 	if !method.GetClientStreaming() && !method.GetServerStreaming() { | ||||
| 		reqArgs = append(reqArgs, "*"+g.typeName(method.GetOutputType())) | ||||
| 	} | ||||
| 	return methName + "(" + strings.Join(reqArgs, ", ") + ") " + ret | ||||
| } | ||||
|  | ||||
| func (g *micro) generateServerMethod(servName string, method *pb.MethodDescriptorProto) string { | ||||
| 	methName := generator.CamelCase(method.GetName()) | ||||
| 	hname := fmt.Sprintf("_%s_%s_Handler", servName, methName) | ||||
| 	serveType := servName + "Handler" | ||||
| 	inType := g.typeName(method.GetInputType()) | ||||
| 	outType := g.typeName(method.GetOutputType()) | ||||
|  | ||||
| 	if !method.GetServerStreaming() && !method.GetClientStreaming() { | ||||
| 		g.P("func (h *", unexport(servName), "Handler) ", methName, "(ctx ", contextPkg, ".Context, in *", inType, ", out *", outType, ") error {") | ||||
| 		g.P("return h.", serveType, ".", methName, "(ctx, in, out)") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
| 		return hname | ||||
| 	} | ||||
| 	streamType := unexport(servName) + methName + "Stream" | ||||
| 	g.P("func (h *", unexport(servName), "Handler) ", methName, "(ctx ", contextPkg, ".Context, stream server.Stream) error {") | ||||
| 	if !method.GetClientStreaming() { | ||||
| 		g.P("m := new(", inType, ")") | ||||
| 		g.P("if err := stream.Recv(m); err != nil { return err }") | ||||
| 		g.P("return h.", serveType, ".", methName, "(ctx, m, &", streamType, "{stream})") | ||||
| 	} else { | ||||
| 		g.P("return h.", serveType, ".", methName, "(ctx, &", streamType, "{stream})") | ||||
| 	} | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	genSend := method.GetServerStreaming() | ||||
| 	genRecv := method.GetClientStreaming() | ||||
|  | ||||
| 	// Stream auxiliary types and methods. | ||||
| 	g.P("type ", servName, "_", methName, "Stream interface {") | ||||
| 	g.P("Context() context.Context") | ||||
| 	g.P("SendMsg(interface{}) error") | ||||
| 	g.P("RecvMsg(interface{}) error") | ||||
| 	g.P("Close() error") | ||||
|  | ||||
| 	if genSend { | ||||
| 		g.P("Send(*", outType, ") error") | ||||
| 	} | ||||
|  | ||||
| 	if genRecv { | ||||
| 		g.P("Recv() (*", inType, ", error)") | ||||
| 	} | ||||
|  | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("type ", streamType, " struct {") | ||||
| 	g.P("stream ", serverPkg, ".Stream") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") Close() error {") | ||||
| 	g.P("return x.stream.Close()") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") Context() context.Context {") | ||||
| 	g.P("return x.stream.Context()") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") SendMsg(m interface{}) error {") | ||||
| 	g.P("return x.stream.Send(m)") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	g.P("func (x *", streamType, ") RecvMsg(m interface{}) error {") | ||||
| 	g.P("return x.stream.Recv(m)") | ||||
| 	g.P("}") | ||||
| 	g.P() | ||||
|  | ||||
| 	if genSend { | ||||
| 		g.P("func (x *", streamType, ") Send(m *", outType, ") error {") | ||||
| 		g.P("return x.stream.Send(m)") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
| 	} | ||||
|  | ||||
| 	if genRecv { | ||||
| 		g.P("func (x *", streamType, ") Recv() (*", inType, ", error) {") | ||||
| 		g.P("m := new(", inType, ")") | ||||
| 		g.P("if err := x.stream.Recv(m); err != nil { return nil, err }") | ||||
| 		g.P("return m, nil") | ||||
| 		g.P("}") | ||||
| 		g.P() | ||||
| 	} | ||||
|  | ||||
| 	return hname | ||||
| } | ||||
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ require ( | ||||
| 	github.com/fsnotify/fsnotify v1.6.0 | ||||
| 	github.com/go-redis/redis/v8 v8.11.5 | ||||
| 	github.com/go-sql-driver/mysql v1.9.2 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.3.0 | ||||
| 	github.com/golang/protobuf v1.5.4 | ||||
| 	github.com/google/uuid v1.6.0 | ||||
| 	github.com/hashicorp/consul/api v1.32.1 | ||||
| @@ -28,9 +29,11 @@ require ( | ||||
| 	github.com/patrickmn/go-cache v2.1.0+incompatible | ||||
| 	github.com/pkg/errors v0.9.1 | ||||
| 	github.com/rabbitmq/amqp091-go v1.10.0 | ||||
| 	github.com/stretchr/objx v0.5.2 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/test-go/testify v1.1.4 | ||||
| 	github.com/urfave/cli/v2 v2.27.6 | ||||
| 	github.com/xlab/treeprint v1.2.0 | ||||
| 	go.etcd.io/bbolt v1.4.0 | ||||
| 	go.etcd.io/etcd/api/v3 v3.5.21 | ||||
| 	go.etcd.io/etcd/client/v3 v3.5.21 | ||||
| @@ -40,6 +43,7 @@ require ( | ||||
| 	golang.org/x/crypto v0.37.0 | ||||
| 	golang.org/x/net v0.38.0 | ||||
| 	golang.org/x/sync v0.13.0 | ||||
| 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 | ||||
| 	google.golang.org/grpc v1.71.1 | ||||
| 	google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057 | ||||
| 	google.golang.org/protobuf v1.36.6 | ||||
| @@ -99,7 +103,6 @@ require ( | ||||
| 	golang.org/x/text v0.24.0 // indirect | ||||
| 	golang.org/x/time v0.11.0 // indirect | ||||
| 	golang.org/x/tools v0.31.0 // indirect | ||||
| 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -79,6 +79,8 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | ||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||
| github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= | ||||
| github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| @@ -339,6 +341,8 @@ github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmN | ||||
| github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= | ||||
| github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= | ||||
| github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= | ||||
| github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= | ||||
| github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= | ||||
| github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= | ||||
| github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= | ||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
|   | ||||
| @@ -327,49 +327,49 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { | ||||
| 		wg.Add(2) | ||||
|  | ||||
| 		// Process the outbound messages from the socket | ||||
| 	       go func(psock *socket.Socket) { | ||||
| 		       defer func() { | ||||
| 			       if r := recover(); r != nil { | ||||
| 				       logger.Log(log.ErrorLevel, "panic recovered in outbound goroutine: ", r) | ||||
| 				       logger.Log(log.ErrorLevel, string(debug.Stack())) | ||||
| 			       } | ||||
| 			       // TODO: don't hack this but if its grpc just break out of the stream | ||||
| 			       // We do this because the underlying connection is h2 and its a stream | ||||
| 			       if protocol == "grpc" { | ||||
| 				       if err := sock.Close(); err != nil { | ||||
| 					       logger.Logf(log.ErrorLevel, "Failed to close socket: %v", err) | ||||
| 				       } | ||||
| 			       } | ||||
| 		go func(psock *socket.Socket) { | ||||
| 			defer func() { | ||||
| 				if r := recover(); r != nil { | ||||
| 					logger.Log(log.ErrorLevel, "panic recovered in outbound goroutine: ", r) | ||||
| 					logger.Log(log.ErrorLevel, string(debug.Stack())) | ||||
| 				} | ||||
| 				// TODO: don't hack this but if its grpc just break out of the stream | ||||
| 				// We do this because the underlying connection is h2 and its a stream | ||||
| 				if protocol == "grpc" { | ||||
| 					if err := sock.Close(); err != nil { | ||||
| 						logger.Logf(log.ErrorLevel, "Failed to close socket: %v", err) | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			       s.deferer(pool, psock, wg) | ||||
| 		       }() | ||||
| 				s.deferer(pool, psock, wg) | ||||
| 			}() | ||||
|  | ||||
| 		       for { | ||||
| 			       // Get the message from our internal handler/stream | ||||
| 			       m := new(transport.Message) | ||||
| 			       if err := psock.Process(m); err != nil { | ||||
| 				       return | ||||
| 			       } | ||||
| 			for { | ||||
| 				// Get the message from our internal handler/stream | ||||
| 				m := new(transport.Message) | ||||
| 				if err := psock.Process(m); err != nil { | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 			       // Send the message back over the socket | ||||
| 			       if err := sock.Send(m); err != nil { | ||||
| 				       return | ||||
| 			       } | ||||
| 		       } | ||||
| 	       }(psock) | ||||
| 				// Send the message back over the socket | ||||
| 				if err := sock.Send(m); err != nil { | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 		}(psock) | ||||
|  | ||||
| 		// Serve the request in a go routine as this may be a stream | ||||
| 	       go func(psock *socket.Socket) { | ||||
| 		       defer func() { | ||||
| 			       if r := recover(); r != nil { | ||||
| 				       logger.Log(log.ErrorLevel, "panic recovered in serveReq goroutine: ", r) | ||||
| 				       logger.Log(log.ErrorLevel, string(debug.Stack())) | ||||
| 			       } | ||||
| 			       s.deferer(pool, psock, wg) | ||||
| 		       }() | ||||
| 		go func(psock *socket.Socket) { | ||||
| 			defer func() { | ||||
| 				if r := recover(); r != nil { | ||||
| 					logger.Log(log.ErrorLevel, "panic recovered in serveReq goroutine: ", r) | ||||
| 					logger.Log(log.ErrorLevel, string(debug.Stack())) | ||||
| 				} | ||||
| 				s.deferer(pool, psock, wg) | ||||
| 			}() | ||||
|  | ||||
| 		       s.serveReq(ctx, msg, &request, &response, rcodec) | ||||
| 	       }(psock) | ||||
| 			s.serveReq(ctx, msg, &request, &response, rcodec) | ||||
| 		}(psock) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,11 +3,11 @@ package transport | ||||
| import "sync" | ||||
|  | ||||
| var http2BufPool = sync.Pool{ | ||||
|        New: func() interface{} { | ||||
|                return make([]byte, DefaultBufSizeH2) | ||||
|        }, | ||||
| 	New: func() interface{} { | ||||
| 		return make([]byte, DefaultBufSizeH2) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func getHTTP2BufPool() *sync.Pool { | ||||
|        return &http2BufPool | ||||
| } | ||||
| 	return &http2BufPool | ||||
| } | ||||
|   | ||||
| @@ -165,51 +165,51 @@ func (h *httpTransportSocket) recvHTTP1(msg *Message) error { | ||||
| } | ||||
|  | ||||
| func (h *httpTransportSocket) recvHTTP2(msg *Message) error { | ||||
|        // only process if the socket is open | ||||
|        select { | ||||
|        case <-h.closed: | ||||
| 	       return io.EOF | ||||
|        default: | ||||
|        } | ||||
| 	// only process if the socket is open | ||||
| 	select { | ||||
| 	case <-h.closed: | ||||
| 		return io.EOF | ||||
| 	default: | ||||
| 	} | ||||
|  | ||||
|        // buffer pool for reuse | ||||
|        var bufPool = getHTTP2BufPool() | ||||
| 	// buffer pool for reuse | ||||
| 	var bufPool = getHTTP2BufPool() | ||||
|  | ||||
|        // set max buffer size | ||||
|        s := h.ht.opts.BuffSizeH2 | ||||
|        if s == 0 { | ||||
| 	       s = DefaultBufSizeH2 | ||||
|        } | ||||
| 	// set max buffer size | ||||
| 	s := h.ht.opts.BuffSizeH2 | ||||
| 	if s == 0 { | ||||
| 		s = DefaultBufSizeH2 | ||||
| 	} | ||||
|  | ||||
|        buf := bufPool.Get().([]byte) | ||||
|        if cap(buf) < s { | ||||
| 	       buf = make([]byte, s) | ||||
|        } | ||||
|        buf = buf[:s] | ||||
| 	buf := bufPool.Get().([]byte) | ||||
| 	if cap(buf) < s { | ||||
| 		buf = make([]byte, s) | ||||
| 	} | ||||
| 	buf = buf[:s] | ||||
|  | ||||
|        n, err := h.buf.Read(buf) | ||||
|        if err != nil { | ||||
| 	       bufPool.Put(buf) | ||||
| 	       return err | ||||
|        } | ||||
| 	n, err := h.buf.Read(buf) | ||||
| 	if err != nil { | ||||
| 		bufPool.Put(buf) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
|        if n > 0 { | ||||
| 	       msg.Body = make([]byte, n) | ||||
| 	       copy(msg.Body, buf[:n]) | ||||
|        } | ||||
|        bufPool.Put(buf) | ||||
| 	if n > 0 { | ||||
| 		msg.Body = make([]byte, n) | ||||
| 		copy(msg.Body, buf[:n]) | ||||
| 	} | ||||
| 	bufPool.Put(buf) | ||||
|  | ||||
|        for k, v := range h.r.Header { | ||||
| 	       if len(v) > 0 { | ||||
| 		       msg.Header[k] = v[0] | ||||
| 	       } else { | ||||
| 		       msg.Header[k] = "" | ||||
| 	       } | ||||
|        } | ||||
| 	for k, v := range h.r.Header { | ||||
| 		if len(v) > 0 { | ||||
| 			msg.Header[k] = v[0] | ||||
| 		} else { | ||||
| 			msg.Header[k] = "" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|        msg.Header[":path"] = h.r.URL.Path | ||||
| 	msg.Header[":path"] = h.r.URL.Path | ||||
|  | ||||
|        return nil | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h *httpTransportSocket) sendHTTP1(msg *Message) error { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user