├── .github └── workflows │ └── default.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── api-node.md ├── app.go ├── app.test.ts ├── app.ts ├── app_test.go ├── certs ├── ca.key ├── ca.pem ├── ca.srl ├── cert.key └── cert.pem ├── cli ├── index.ts └── ngrpc │ ├── cmd │ ├── cert.go │ ├── host.go │ ├── init.go │ ├── list.go │ ├── protoc.go │ ├── reload.go │ ├── restart.go │ ├── root.go │ ├── run.go │ ├── start.go │ └── stop.go │ └── main.go ├── cli_test.go ├── config ├── config.go ├── config_test.go ├── tsconfig.go └── tsconfig_test.go ├── entry ├── main.go └── main.ts ├── go.mod ├── go.sum ├── index.ts ├── ngrpc.json ├── ngrpc.schema.json ├── pack.js ├── package-lock.json ├── package.json ├── pm ├── guest.go ├── guest.test.ts ├── guest.ts ├── guest_test.go ├── host.go ├── host_test.go ├── socket │ ├── unix.go │ └── windows.go └── unix_test.go ├── pm2.config.js ├── proto ├── ExampleService.proto └── github │ └── ayonli │ └── ngrpc │ └── services │ ├── PostService.proto │ ├── UserService.proto │ └── struct.proto ├── scripts ├── main.go └── main.ts ├── services ├── ExampleService.go ├── ExampleService.ts ├── PostService.go ├── PostService.ts ├── UserService.go ├── UserService.ts ├── github │ └── ayonli │ │ └── ngrpc │ │ └── services_proto │ │ ├── PostService.pb.go │ │ ├── PostService_grpc.pb.go │ │ ├── UserService.pb.go │ │ ├── UserService_grpc.pb.go │ │ └── struct.pb.go ├── proto │ ├── ExampleService.pb.go │ └── ExampleService_grpc.pb.go └── struct.ts ├── tsconfig.json ├── unix_test.go ├── util ├── index.test.ts ├── index.ts ├── util.go └── util_test.go └── web ├── main.go └── main.ts /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Node and Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '20.x' 24 | cache: 'npm' 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: '1.21' 30 | 31 | - run: npm i 32 | - run: npx tsc 33 | - run: go mod tidy 34 | - run: go install github.com/ayonli/ngrpc/cli/ngrpc 35 | 36 | - name: Test Node.js 37 | run: npm test 38 | - name: Test Go:util 39 | run: go test -v --timeout 60s ./util 40 | - name: Test Go:config 41 | run: go test -v --timeout 60s ./config 42 | - name: Test Go:pm 43 | run: go test -v --timeout 60s ./pm 44 | - name: Test Go 45 | run: go test --timeout 60s -v . 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.tgz 3 | *.log 4 | .vscode/ 5 | dist/ 6 | prebuild/ 7 | !postpublish.js 8 | !pm2.config.js 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.sh 3 | *.tgz 4 | *.log 5 | *.test.ts 6 | *.test.js 7 | *.test.js.map 8 | *.go 9 | go.* 10 | tsconfig.tsbuildinfo 11 | tsconfig.json 12 | ngrpc.json 13 | ngrpc.schema.json 14 | pm2.config.js 15 | pack.js 16 | certs/ 17 | cli/ 18 | entry/ 19 | proto/ 20 | services/ 21 | scripts/ 22 | web/ 23 | dist/cli/ngrpc 24 | dist/entry/ 25 | dist/services/ 26 | dist/scripts/ 27 | dist/web/ 28 | prebuild/ 29 | api-node.md 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 A-yon Lee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /api-node.md: -------------------------------------------------------------------------------- 1 | ## Programmatic API for Node.js 2 | 3 | **`service(name: string): ClassDecorator`** 4 | 5 | This decorator function is used to link the service class to a gRPC service. 6 | 7 | - `name` The service name defined in the `.proto` file. 8 | 9 | ---- 10 | 11 | **`ngrpc.start(appName?: string): Promise`** 12 | 13 | Initiates an app by the given name and loads the config file, it initiates the server (if served) 14 | and client connections, prepares the services ready for use. 15 | 16 | *NOTE: There can only be one named app running in the same process.* 17 | 18 | - `appName` The app's name that should be started as a server. If not provided, the app only 19 | connects to other servers but not serves as one. 20 | 21 | **Example** 22 | 23 | ```ts 24 | import ngrpc from "@ayonli/ngrpc"; 25 | 26 | (async () => { 27 | // This app starts a gRPC server named 'example-server' and connects to all services. 28 | const serverApp = await ngrpc.start("example-server"); 29 | })(); 30 | 31 | (async () => { 32 | // This app won't start a gRPC server, but connects to all services. 33 | const clientApp = await ngrpc.start(); 34 | })(); 35 | ``` 36 | 37 | ---- 38 | 39 | **`ngrpc.startWithConfig(appName: string | null, config: Config): Promise`** 40 | 41 | Like `start()` except it takes a config argument instead of loading the config file. 42 | 43 | ---- 44 | 45 | **`app.stop(): Promise`** 46 | 47 | Closes client connections and stops the server (if served), and runs any `destroy()` method in the 48 | bound services. 49 | 50 | **Example** 51 | 52 | ```ts 53 | import ngrpc from "@ayonli/ngrpc"; 54 | 55 | ngrpc.start("example-server").then(app => { 56 | process.on("exit", (code) => { 57 | // Stop the app when the program is issued to exit. 58 | app.stop().then(() => { 59 | process.exit(code); 60 | }); 61 | }); 62 | }); 63 | ``` 64 | 65 | ---- 66 | 67 | **`app.reload(): Promise`** 68 | 69 | Reloads the app programmatically. 70 | 71 | This function is rarely used explicitly, prefer the CLI `reload` command instead. 72 | 73 | ---- 74 | 75 | **`app.waitForExit(): void`** 76 | 77 | Listens for system signals to exit the program. 78 | 79 | This method calls the `stop()` method internally, if we don't use this method, we need to 80 | call the `stop()` method explicitly when the program is going to terminate. 81 | 82 | ---- 83 | 84 | **`app.onStop(callback: () => void | Promise): void`** 85 | 86 | Registers a callback to run after the app is stopped. 87 | 88 | **Example** 89 | 90 | ```ts 91 | import ngrpc from "@ayonli/ngrpc"; 92 | 93 | ngrpc.start("example-server").then(app => { 94 | app.onStop(() => { 95 | // Terminate the process when the app is stopped. 96 | process.exit(0); 97 | }); 98 | }); 99 | ``` 100 | 101 | ---- 102 | 103 | **`app.onReload(callback: () => void | Promise): void`** 104 | 105 | Registers a callback to run after the app is reloaded. 106 | 107 | **Example** 108 | 109 | ```ts 110 | import ngrpc from "@ayonli/ngrpc"; 111 | 112 | ngrpc.start("example-server").then(app => { 113 | app.onReload(() => { 114 | // Log the reload event. 115 | console.info("The app has been reloaded"); 116 | }); 117 | }); 118 | ``` 119 | 120 | ---- 121 | 122 | **`ngrpc.loadConfig(): Promise`** 123 | 124 | Loads the configurations. 125 | 126 | ---- 127 | 128 | **`ngrpc.loadConfigForPM2(): { apps: any[] }`** 129 | 130 | Loads the configurations and reorganizes them so that the same configurations can be used in PM2's 131 | configuration file. 132 | 133 | ---- 134 | 135 | **`ngrpc.getAppName(): string`** 136 | 137 | Retrieves the app name from the `process.argv`. 138 | 139 | ---- 140 | 141 | **`ngrpc.getServiceClient(serviceName: string, route?: string): ServiceClient** 142 | 143 | Returns the service client by the given service name. 144 | 145 | - `route` is used to route traffic by the client-side load balancer. 146 | 147 | ---- 148 | 149 | **`ngrpc.runSnippet(fn: () => void | Promise): Promise`** 150 | 151 | Runs a snippet inside the apps context. 152 | 153 | This function is for temporary scripting usage, it starts a temporary pure-clients app so we can use 154 | the services as we normally do in our program, and after the main `fn` function is run, the app is 155 | automatically stopped. 156 | 157 | - `fn` The function to be run. 158 | 159 | **Example** 160 | 161 | ```ts 162 | import ngrpc from "@ayonli/ngrpc"; 163 | 164 | ngrpc.runSnippet(async () => { 165 | const post = await services.PostService.getPost({ id: 1 }); 166 | console.log(post); 167 | }); 168 | ``` 169 | -------------------------------------------------------------------------------- /app.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { test } from "mocha"; 3 | import * as assert from "node:assert"; 4 | import * as fs from "node:fs/promises"; 5 | import * as path from "node:path"; 6 | import { spawnSync } from "node:child_process"; 7 | import { sleep } from "@ayonli/jsext/async"; 8 | import _try from "@ayonli/jsext/try"; 9 | import func from "@ayonli/jsext/func"; 10 | import ngrpc, { Config, RpcApp } from "./app"; 11 | 12 | test("ngrpc.loadConfig", async () => { 13 | const config = await ngrpc.loadConfig(); 14 | assert.ok(config.apps.length > 0); 15 | }); 16 | 17 | test("ngrpc.localConfig with local config file", async () => { 18 | await fs.copyFile("ngrpc.json", "ngrpc.local.json"); 19 | const [err, config] = await _try(ngrpc.loadConfig()); 20 | await fs.unlink("ngrpc.local.json"); 21 | 22 | assert.ok(!err); 23 | assert.ok(config.apps.length > 0); 24 | }); 25 | 26 | test("ngrpc.loadConfig with failure", async () => { 27 | await fs.rename("ngrpc.json", "ngrpc.jsonc"); 28 | const [err, config] = await _try(ngrpc.loadConfig()); 29 | await fs.rename("ngrpc.jsonc", "ngrpc.json"); 30 | 31 | const filename = path.join(process.cwd(), "ngrpc.json"); 32 | assert.strictEqual(err?.message, `unable to load config file: ${filename}`); 33 | assert.ok(!config); 34 | }); 35 | 36 | test("ngrpc.loadConfigForPM2", async () => { 37 | const cfg = await ngrpc.loadConfig(); 38 | const pm2Cfg = ngrpc.loadConfigForPM2(); 39 | 40 | assert.ok(pm2Cfg.apps.length > 0); 41 | 42 | for (const pm2App of pm2Cfg.apps) { 43 | if (!pm2App.script) { 44 | throw new Error("the app's script cannot be empty"); 45 | } 46 | 47 | const ext = path.extname(pm2App.script); 48 | const app = cfg.apps.find(item => item.name === pm2App.name); 49 | 50 | if (!app) { 51 | throw new Error(`app [${pm2App.name}] is not found`); 52 | } 53 | 54 | if (app.name.includes(" ")) { 55 | assert.strictEqual(pm2App.args, `"${app.name}"`); 56 | } else { 57 | assert.strictEqual(pm2App.args, app.name); 58 | } 59 | 60 | assert.deepStrictEqual(pm2App.env ?? {}, app.env ?? {}); 61 | 62 | if (app.stdout && app.stderr) { 63 | assert.strictEqual(pm2App.out_file, app.stdout); 64 | assert.strictEqual(pm2App.err_file, app.stderr); 65 | } else if (app.stdout && !app.stderr) { 66 | assert.strictEqual(pm2App.log_file, app.stdout); 67 | } 68 | 69 | if (ext === ".js") { 70 | assert.strictEqual(pm2App.interpreter_args, "-r source-map-support/register"); 71 | } else if (ext === ".ts") { 72 | assert.strictEqual(pm2App.interpreter_args, "-r ts-node/register"); 73 | } else if (ext === ".go") { 74 | assert.strictEqual(pm2App.interpreter, "go"); 75 | assert.strictEqual(pm2App.interpreter_args, "run"); 76 | } else if (ext === ".exe" || !ext) { 77 | assert.strictEqual(pm2App.interpreter, "none"); 78 | } else { 79 | throw new Error(`entry file '${app.entry}' of app [${app.name}] is recognized`); 80 | } 81 | } 82 | }); 83 | 84 | test("ngrpc.start", func(async (defer) => { 85 | const app = await ngrpc.start("example-server"); 86 | defer(() => app.stop()); 87 | 88 | assert.strictEqual(app.name, "example-server"); 89 | 90 | const reply = await services.ExampleService.sayHello({ name: "World" }); 91 | assert.strictEqual(reply.message, "Hello, World"); 92 | })); 93 | 94 | test("ngrpc.start without app name", func(async function (defer) { 95 | this.timeout(5_000); 96 | 97 | spawnSync("ngrpc", ["start", "example-server"]); 98 | defer(async () => { 99 | spawnSync("ngrpc", ["stop"]); 100 | await sleep(10); 101 | }); 102 | 103 | const app = await ngrpc.start(); 104 | defer(() => app.stop()); 105 | 106 | const reply = await services.ExampleService.sayHello({ name: "World" }); 107 | assert.strictEqual(reply.message, "Hello, World"); 108 | })); 109 | 110 | 111 | test("ngrpc.startWithConfig", func(async (defer) => { 112 | const cfg = await ngrpc.loadConfig(); 113 | const app = await ngrpc.startWithConfig("example-server", cfg); 114 | defer(() => app.stop()); 115 | 116 | assert.strictEqual(app.name, "example-server"); 117 | 118 | const reply = await services.ExampleService.sayHello({ name: "World" }); 119 | assert.strictEqual(reply.message, "Hello, World"); 120 | })); 121 | 122 | test("ngrpc.startWithConfig with xds protocol", async () => { 123 | const cfg = await ngrpc.loadConfig(); 124 | const cfgApp = cfg.apps.find(item => item.name === "example-server"); 125 | 126 | if (!cfgApp) { 127 | throw new Error("app [example-server] not found"); 128 | } 129 | 130 | cfgApp.entry = "entry/main.ts"; 131 | cfgApp.url = "xds://localhost:4000"; 132 | 133 | const [err, app] = await _try(ngrpc.startWithConfig("example-server", cfg)); 134 | 135 | assert.ok(!app); 136 | assert.strictEqual(err?.message, 137 | `app [example-server] cannot be served since it uses 'xds:' protocol`); 138 | }); 139 | 140 | test("ngrpc.start invalid app", async () => { 141 | const [err, app] = await _try(ngrpc.start("test-server")); 142 | 143 | assert.ok(!app); 144 | assert.strictEqual(err?.message, "app [test-server] is not configured"); 145 | }); 146 | 147 | test("ngrpc.startWithConfig with invalid URl", async () => { 148 | const cfg = await ngrpc.loadConfig(); 149 | const cfgApp = cfg.apps.find(item => item.name === "example-server"); 150 | 151 | if (!cfgApp) { 152 | throw new Error("app [example-server] not found"); 153 | } 154 | 155 | cfgApp.entry = "entry/main.ts"; 156 | cfgApp.url = "grpc://localhost:abc"; 157 | 158 | const [err, app] = await _try(ngrpc.startWithConfig("example-server", cfg)); 159 | 160 | assert.ok(!app); 161 | assert.strictEqual(err?.message, `Invalid URL`); 162 | }); 163 | 164 | test("ngrpc.start duplicated call", func(async (defer) => { 165 | const app1 = await ngrpc.start("example-server"); 166 | const [err, app2] = await _try(ngrpc.start("post-server")); 167 | defer(() => app1.stop()); 168 | 169 | assert.ok(!app2); 170 | assert.strictEqual(err?.message, "an app is already running"); 171 | })); 172 | 173 | test("ngrpc.getServiceClient", func(async (defer) => { 174 | const app = await ngrpc.start("post-server"); 175 | defer(() => app.stop()); 176 | 177 | const ins1 = ngrpc.getServiceClient("services.PostService"); 178 | const ins2 = ngrpc.getServiceClient("services.PostService", "post-server"); 179 | const ins3 = ngrpc.getServiceClient("services.PostService", "grpcs://localhost:4002"); 180 | 181 | assert.ok(!!ins1); 182 | assert.ok(!!ins2); 183 | assert.ok(!!ins3); 184 | })); 185 | 186 | test("ngrpc.runSnippet", func(async function (defer) { 187 | this.timeout(5_000); 188 | 189 | spawnSync("ngrpc", ["start", "example-server"]); 190 | defer(async () => { 191 | spawnSync("ngrpc", ["stop"]); 192 | await sleep(10); 193 | }); 194 | 195 | let message: string | undefined; 196 | await ngrpc.runSnippet(async () => { 197 | const reply = await services.ExampleService.sayHello({ name: "World" }); 198 | message = reply.message; 199 | }); 200 | 201 | assert.strictEqual(message, "Hello, World"); 202 | })); 203 | 204 | test("app.stop and app.onStop", async () => { 205 | const app = await ngrpc.start("example-server"); 206 | let stopped = false; 207 | 208 | app.onStop(() => { 209 | stopped = true; 210 | }); 211 | 212 | await app.stop(); 213 | assert.ok(stopped); 214 | }); 215 | 216 | test("app.reload and app.onReload", func(async (defer) => { 217 | const app = await ngrpc.start("example-server"); 218 | defer(() => app.stop()); 219 | 220 | let reloaded = false; 221 | 222 | app.onReload(() => { 223 | reloaded = true; 224 | }); 225 | 226 | await app.reload(); 227 | assert.ok(reloaded); 228 | })); 229 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package ngrpc_test 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "testing" 7 | "time" 8 | 9 | "github.com/ayonli/goext" 10 | "github.com/ayonli/ngrpc" 11 | "github.com/ayonli/ngrpc/config" 12 | "github.com/ayonli/ngrpc/services" 13 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 14 | "github.com/ayonli/ngrpc/services/proto" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestStart(t *testing.T) { 19 | app := goext.Ok(ngrpc.Start("user-server")) 20 | defer app.Stop() 21 | 22 | assert.Equal(t, "user-server", app.Name) 23 | 24 | userId := "ayon.li" 25 | userSrv := goext.Ok((&services.UserService{}).GetClient("")) 26 | user := goext.Ok(userSrv.GetUser(context.Background(), &services_proto.UserQuery{Id: &userId})) 27 | 28 | assert.Equal(t, "A-yon Lee", user.Name) 29 | } 30 | 31 | func TestStartWithoutAppName(t *testing.T) { 32 | goext.Ok(0, exec.Command("ngrpc", "start", "example-server").Run()) 33 | defer func() { 34 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 35 | time.Sleep(time.Millisecond * 10) 36 | }() 37 | 38 | app := goext.Ok(ngrpc.Start("")) 39 | defer app.Stop() 40 | 41 | srv := goext.Ok((&services.ExampleService{}).GetClient("")) 42 | reply := goext.Ok(srv.SayHello(context.Background(), &proto.HelloRequest{Name: "World"})) 43 | assert.Equal(t, "Hello, World", reply.Message) 44 | } 45 | 46 | func TestStartWithConfig(t *testing.T) { 47 | cfg := goext.Ok(config.LoadConfig()) 48 | app := goext.Ok(ngrpc.StartWithConfig("user-server", cfg)) 49 | defer app.Stop() 50 | 51 | assert.Equal(t, "user-server", app.Name) 52 | 53 | userId := "ayon.li" 54 | userSrv := goext.Ok((&services.UserService{}).GetClient("")) 55 | user := goext.Ok(userSrv.GetUser(context.Background(), &services_proto.UserQuery{Id: &userId})) 56 | 57 | assert.Equal(t, "A-yon Lee", user.Name) 58 | } 59 | 60 | func TestStartWithConfigWithXdsProtocol(t *testing.T) { 61 | cfg := config.Config{ 62 | Apps: []config.App{ 63 | { 64 | Name: "example-server", 65 | Url: "xds://localhost:5001", 66 | Serve: true, 67 | Services: []string{ 68 | "services.ExampleService", 69 | }, 70 | }, 71 | }, 72 | } 73 | app, err := ngrpc.StartWithConfig("example-server", cfg) 74 | 75 | assert.Nil(t, app) 76 | assert.Equal(t, 77 | "app [example-server] cannot be served since it uses 'xds:' protocol", 78 | err.Error()) 79 | } 80 | 81 | func TestStartInvalidApp(t *testing.T) { 82 | app, err := ngrpc.Start("test-server") 83 | 84 | assert.Nil(t, app) 85 | assert.Equal(t, "app [test-server] is not configured", err.Error()) 86 | } 87 | 88 | func TestStartInvalidUrl(t *testing.T) { 89 | cfg := config.Config{ 90 | Apps: []config.App{ 91 | { 92 | Name: "example-server", 93 | Url: "grpc://localhost:abc", 94 | Serve: true, 95 | Services: []string{ 96 | "services.ExampleService", 97 | }, 98 | }, 99 | }, 100 | } 101 | 102 | app, err := ngrpc.StartWithConfig("example-server", cfg) 103 | 104 | assert.Nil(t, app) 105 | assert.Equal(t, "parse \"grpc://localhost:abc\": invalid port \":abc\" after host", err.Error()) 106 | } 107 | 108 | func TestStartDuplicateCall(t *testing.T) { 109 | app1 := goext.Ok(ngrpc.Start("user-server")) 110 | app2, err := ngrpc.Start("user-server") 111 | defer app1.Stop() 112 | 113 | assert.Nil(t, app2) 114 | assert.Equal(t, "an app is already running", err.Error()) 115 | } 116 | 117 | func TestGetServiceClient(t *testing.T) { 118 | app := goext.Ok(ngrpc.Start("user-server")) 119 | defer app.Stop() 120 | 121 | ins1 := goext.Ok(ngrpc.GetServiceClient(&services.UserService{}, "")) 122 | ins2 := goext.Ok(ngrpc.GetServiceClient(&services.UserService{}, "user-server")) 123 | ins3 := goext.Ok(ngrpc.GetServiceClient(&services.UserService{}, "grpcs://localhost:4001")) 124 | 125 | assert.NotNil(t, ins1) 126 | assert.NotNil(t, ins2) 127 | assert.NotNil(t, ins3) 128 | assert.Equal(t, ins1, ins2) 129 | assert.Equal(t, ins1, ins3) 130 | } 131 | 132 | func TestForSnippet(t *testing.T) { 133 | goext.Ok(0, exec.Command("ngrpc", "start", "example-server").Run()) 134 | done := ngrpc.ForSnippet() 135 | defer done() 136 | 137 | ctx := context.Background() 138 | ins := goext.Ok((&services.ExampleService{}).GetClient("")) 139 | text := goext.Ok(ins.SayHello(ctx, &proto.HelloRequest{Name: "A-yon Lee"})) 140 | 141 | assert.Equal(t, "Hello, A-yon Lee", text.Message) 142 | 143 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 144 | time.Sleep(time.Millisecond * 10) 145 | } 146 | 147 | func TestStopAndOnStop(t *testing.T) { 148 | app := goext.Ok(ngrpc.Start("user-server")) 149 | stopped := false 150 | 151 | app.OnStop(func() { 152 | stopped = true 153 | }) 154 | 155 | app.Stop() 156 | assert.True(t, stopped) 157 | } 158 | -------------------------------------------------------------------------------- /certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | H7S2jpqYErYMAOg0QStuVVDMTxgb+w== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 33 | WKrIWtVcOjBvfbUkvA== 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /certs/ca.srl: -------------------------------------------------------------------------------- 1 | 0FAC684D0246380C5CB9A3B0A5BD69C809EA5A36 2 | -------------------------------------------------------------------------------- /certs/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | kmEyTfXZh0sgx0Z+HrYawxwBKRKE5A== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 33 | 0rcbIRfaUZd/hKSaHfq/uMFUp0DXKA== 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { spawnSync } from "node:child_process"; 3 | import * as path from "node:path"; 4 | import * as http from "node:http"; 5 | import { https } from "follow-redirects"; 6 | import { exists, remove } from "@ayonli/jsext/fs"; 7 | import { readAsJSON } from "@ayonli/jsext/reader"; 8 | import * as tar from "tar"; 9 | 10 | const exePath = path.join(__dirname, process.platform === "win32" ? "ngrpc.exe" : "ngrpc"); 11 | const os = process.platform === "win32" ? "windows" : process.platform; 12 | const arch = process.arch === "x64" ? "amd64" : process.arch; 13 | const zipName = `ngrpc-${os}-${arch}.tgz`; 14 | 15 | function reportImportFailure(err?: Error) { 16 | if (err) { 17 | console.error(err); 18 | console.error(""); 19 | } 20 | 21 | console.error("cannot import ngrpc executable, try install it via:"); 22 | console.error(" go install github.com/ayonli/ngrpc/cli/ngrpc@latest"); 23 | process.exit(1); 24 | } 25 | 26 | (async function main() { 27 | if (!(await exists(exePath))) { 28 | if (!zipName) { 29 | reportImportFailure(); 30 | } 31 | 32 | const version = await new Promise((resolve, reject) => { 33 | https.get("https://api.github.com/repos/ayonli/ngrpc/releases/latest", { 34 | headers: { 35 | "User-Agent": "Node.js", 36 | }, 37 | }, async res => { 38 | const data = await readAsJSON(res) as { 39 | tag_name: string; 40 | }; 41 | resolve(data.tag_name); 42 | }).once("error", reject); 43 | }); 44 | const url = `https://github.com/ayonli/ngrpc/releases/download/${version}/${zipName}`; 45 | const res = await new Promise((resolve, reject) => { 46 | https.get(url, res => { 47 | resolve(res); 48 | }).once("error", reject); 49 | }); 50 | 51 | if (res.statusCode !== 200) { 52 | reportImportFailure(new Error(`unable to download ${zipName}`)); 53 | } 54 | 55 | await new Promise((resolve, reject) => { 56 | const out = tar.extract({ cwd: __dirname }); 57 | const handleError = async (err: Error) => { 58 | try { await remove(exePath); } catch { } 59 | reject(err); 60 | }; 61 | 62 | res.pipe(out); 63 | res.on("error", handleError); 64 | out.on("error", handleError).on("finish", resolve); 65 | }); 66 | 67 | spawnSync(exePath, process.argv.slice(2), { stdio: "inherit" }); 68 | } else { 69 | spawnSync(exePath, process.argv.slice(2), { stdio: "inherit" }); 70 | } 71 | })().catch(err => { 72 | reportImportFailure(err); 73 | }); 74 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/cert.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/ayonli/goext" 14 | "github.com/ayonli/goext/slicex" 15 | "github.com/ayonli/goext/stringx" 16 | "github.com/ayonli/ngrpc/util" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | var certCmd = &cobra.Command{ 21 | Use: "cert ", 22 | Short: "generate a pair of self-signed certificate", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if _, err := exec.LookPath("openssl"); err != nil { 25 | fmt.Println("openssl not found, please install it before generating certificates") 26 | return 27 | } else if len(args) < 1 { 28 | fmt.Println("the out file must be provided") 29 | return 30 | } 31 | 32 | caPem := cmd.Flag("ca").Value.String() 33 | caKey := cmd.Flag("caKey").Value.String() 34 | outPem := args[0] 35 | ext := filepath.Ext(outPem) 36 | 37 | if ext != ".pem" { 38 | fmt.Println("the out file must be suffixed with .pem") 39 | return 40 | } 41 | 42 | dir := filepath.Dir(outPem) 43 | util.EnsureDir(dir) 44 | outKey := stringx.Slice(outPem, 0, -len(ext)) + ".key" 45 | subjFields := []string{"C", "ST", "L", "O", "OU", "CN", "emailAddress"} 46 | subjValues := []string{} 47 | subjPairs := []string{} 48 | 49 | if util.Exists(caPem) { 50 | if !util.Exists(caKey) { 51 | fmt.Println("both ca.pem and ca.key must either exist or not exist") 52 | return 53 | } 54 | } else if !util.Exists(caPem) { 55 | _cmd := exec.Command( 56 | "openssl", 57 | "req", 58 | "-x509", 59 | "-newkey", 60 | "rsa:4096", 61 | "-nodes", 62 | "-days", 63 | "365", 64 | "-keyout", 65 | caKey, 66 | "--out", 67 | caPem) 68 | _cmd.Stdout = os.Stdout 69 | _cmd.Stderr = os.Stderr 70 | writer := goext.Ok(_cmd.StdinPipe()) 71 | reader := bufio.NewReader(os.Stdin) 72 | 73 | go func() { 74 | for { 75 | if _cmd.ProcessState != nil && _cmd.ProcessState.Exited() { 76 | break 77 | } 78 | 79 | bytes, err := reader.ReadBytes('\n') 80 | 81 | if err != nil { 82 | break 83 | } 84 | 85 | subjValues = append(subjValues, string(bytes[0:len(bytes)-1])) 86 | writer.Write(bytes) 87 | } 88 | }() 89 | 90 | goext.Ok(0, _cmd.Run()) 91 | } 92 | 93 | _cmd := exec.Command( 94 | "openssl", 95 | "req", 96 | "-newkey", 97 | "rsa:4096", 98 | "-nodes", 99 | "-days", 100 | "365", 101 | "-keyout", 102 | outKey, 103 | "--out", 104 | outPem) 105 | 106 | _cmd.Stdout = os.Stdout 107 | _cmd.Stderr = os.Stderr 108 | 109 | if len(subjValues) > 0 { 110 | for i, info := range subjValues { 111 | field := subjFields[i] 112 | subjPairs = append(subjPairs, field+"="+info) 113 | } 114 | 115 | _cmd.Args = append(_cmd.Args, "-subj", "/"+strings.Join(subjPairs, "/")) 116 | _cmd.Stdin = os.Stdin 117 | } else { 118 | writer := goext.Ok(_cmd.StdinPipe()) 119 | reader := bufio.NewReader(os.Stdin) 120 | 121 | go func() { 122 | for { 123 | if _cmd.ProcessState != nil && _cmd.ProcessState.Exited() { 124 | break 125 | } 126 | 127 | bytes, err := reader.ReadBytes('\n') 128 | 129 | if err != nil { 130 | break 131 | } 132 | 133 | subjValues = append(subjValues, string(bytes[0:len(bytes)-1])) 134 | writer.Write(bytes) 135 | } 136 | }() 137 | } 138 | 139 | goext.Ok(0, _cmd.Run()) 140 | hostname := subjValues[5] 141 | 142 | if hostname == "" { 143 | fmt.Println("Did you forget the Common Name in the previous steps? Try again!") 144 | return 145 | } 146 | 147 | ips := goext.Ok(net.LookupIP(hostname)) 148 | var ip string 149 | 150 | if len(ips) > 1 { 151 | _ip, ok := slicex.Find(ips, func(item net.IP, idx int) bool { 152 | return stringx.Search(item.String(), `^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$`) != -1 153 | }) 154 | 155 | if ok { 156 | ip = _ip.String() 157 | } else { 158 | ip = ips[0].String() 159 | } 160 | } else { 161 | ip = ips[0].String() 162 | } 163 | 164 | randId := stringx.Random(4) 165 | extCfgFile := path.Join(dir, randId+".cfg") 166 | extCfgContent := fmt.Sprintf("subjectAltName=DNS:%s,IP:%s\n", hostname, ip) 167 | os.WriteFile(extCfgFile, []byte(extCfgContent), 0644) 168 | defer os.Remove(extCfgFile) 169 | 170 | _cmd = exec.Command( 171 | "openssl", 172 | "x509", 173 | "-req", 174 | "-in", 175 | outPem, 176 | "-CA", 177 | caPem, 178 | "-CAkey", 179 | caKey, 180 | "-CAcreateserial", 181 | "-out", 182 | outPem, 183 | "-extfile", 184 | extCfgFile) 185 | _cmd.Stdin = os.Stdin 186 | _cmd.Stdout = os.Stdout 187 | _cmd.Stderr = os.Stderr 188 | 189 | goext.Ok(0, _cmd.Run()) 190 | }, 191 | } 192 | 193 | func init() { 194 | rootCmd.AddCommand(certCmd) 195 | certCmd.Flags().String( 196 | "ca", 197 | "certs/ca.pem", 198 | "use a ca.pem for signing, if doesn't exist, it will be auto-generated") 199 | certCmd.Flags().String( 200 | "caKey", 201 | "certs/ca.key", 202 | "use a ca.key for signing, if doesn't exist, it will be auto-generated") 203 | } 204 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/host.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/ayonli/goext" 11 | "github.com/ayonli/ngrpc/pm" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var hostCmd = &cobra.Command{ 16 | Use: "host", 17 | Short: "start the host server in standalone mode", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | flag := cmd.Flag("stop") 20 | 21 | if flag != nil && flag.Value.String() == "true" { 22 | if !pm.IsHostOnline() { 23 | fmt.Println("host server is not running") 24 | } else { 25 | pm.SendCommand("stop-host", "") 26 | log.Println("host server shut down") 27 | } 28 | } else if pm.IsHostOnline() { 29 | fmt.Println("host server is already running") 30 | } else { 31 | err := startHost(true) 32 | 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | } 37 | }, 38 | } 39 | 40 | func init() { 41 | rootCmd.AddCommand(hostCmd) 42 | hostCmd.Flags().Bool("stop", false, "stop the host server") 43 | } 44 | 45 | func startHost(standalone bool) error { 46 | cmd := exec.Command(os.Args[0], "host-server") 47 | 48 | if standalone { 49 | cmd.Args = append(cmd.Args, "--standalone") 50 | } 51 | 52 | cmd.Stdout = goext.Ok(os.OpenFile("host.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)) 53 | cmd.Stderr = goext.Ok(os.OpenFile("host.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)) 54 | err := cmd.Start() 55 | 56 | if err != nil { 57 | return err 58 | } else { 59 | log.Printf("host server started (pid: %d)", cmd.Process.Pid) 60 | cmd.Process.Release() 61 | time.Sleep(time.Millisecond * 200) // wait a while for the host server to serve 62 | return nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/ayonli/goext/stringx" 10 | "github.com/ayonli/ngrpc/util" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var tsConfTpl = `{ 15 | "compilerOptions": { 16 | "module": "commonjs", 17 | "target": "es2018", 18 | "newLine": "LF", 19 | "incremental": true, 20 | "importHelpers": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "noUnusedParameters": true, 24 | "noUnusedLocals": true, 25 | "noImplicitAny": true, 26 | "noImplicitThis": true, 27 | "noImplicitOverride": true, 28 | "noImplicitReturns": true, 29 | "noFallthroughCasesInSwitch": true, 30 | "noPropertyAccessFromIndexSignature": true, 31 | "noUncheckedIndexedAccess": true 32 | }, 33 | "include": [ 34 | "*.ts", 35 | "*/**.ts" 36 | ] 37 | } 38 | ` 39 | 40 | var confTpl = `{ 41 | "$schema": "https://raw.githubusercontent.com/ayonli/ngrpc/main/ngrpc.schema.json", 42 | "protoPaths": [ 43 | "proto" 44 | ], 45 | "protoOptions": { 46 | "defaults": true 47 | }, 48 | "apps": [ 49 | { 50 | "name": "example-server", 51 | "url": "grpc://localhost:4000", 52 | "serve": true, 53 | "services": [ 54 | "services.ExampleService" 55 | ], 56 | "entry": "main.go", 57 | "stdout": "out.log" 58 | } 59 | ] 60 | }` 61 | 62 | var entryGoTpl = `package main 63 | 64 | import ( 65 | "log" 66 | 67 | "github.com/ayonli/ngrpc" 68 | _ "github.com/ayonli/ngrpc/services" 69 | ) 70 | 71 | func main() { 72 | app, err := ngrpc.Start(ngrpc.GetAppName()) 73 | 74 | if err != nil { 75 | log.Fatal(err) 76 | } else { 77 | app.WaitForExit() 78 | } 79 | } 80 | ` 81 | 82 | var entryTsTpl = `import ngrpc from "@ayonli/ngrpc"; 83 | 84 | if (require.main?.filename === __filename) { 85 | ngrpc.start(ngrpc.getAppName()).then(app => { 86 | process.send?.("ready"); // for PM2 87 | app.waitForExit(); 88 | }).catch(err => { 89 | console.error(err); 90 | process.exit(1); 91 | }); 92 | } 93 | ` 94 | 95 | var exampleProtoTpl = `syntax = "proto3"; 96 | 97 | option go_package = "./proto"; 98 | 99 | package services; 100 | 101 | message HelloRequest { 102 | string name = 1; 103 | } 104 | 105 | message HelloReply { 106 | string message = 2; 107 | } 108 | 109 | service ExampleService { 110 | rpc sayHello(HelloRequest) returns (HelloReply) {} 111 | } 112 | ` 113 | 114 | var exampleServiceGoTpl = `package services 115 | 116 | import ( 117 | "context" 118 | 119 | "github.com/ayonli/ngrpc" 120 | "github.com/ayonli/ngrpc/services/proto" 121 | "google.golang.org/grpc" 122 | ) 123 | 124 | type ExampleService struct { 125 | proto.UnimplementedExampleServiceServer 126 | } 127 | 128 | func (self *ExampleService) Serve(s grpc.ServiceRegistrar) { 129 | proto.RegisterExampleServiceServer(s, self) 130 | } 131 | 132 | func (self *ExampleService) Connect(cc grpc.ClientConnInterface) proto.ExampleServiceClient { 133 | return proto.NewExampleServiceClient(cc) 134 | } 135 | 136 | func (self *ExampleService) GetClient(route string) (proto.ExampleServiceClient, error) { 137 | return ngrpc.GetServiceClient(self, route) 138 | } 139 | 140 | func (self *ExampleService) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) { 141 | return &proto.HelloReply{Message: "Hello, " + req.Name}, nil 142 | } 143 | 144 | func init() { 145 | ngrpc.Use(&ExampleService{}) 146 | } 147 | ` 148 | 149 | var exampleServiceTsTpl = `import { ServiceClient, service } from "@ayonli/ngrpc"; 150 | 151 | declare global { 152 | namespace services { 153 | const ExampleService: ServiceClient; 154 | } 155 | } 156 | 157 | export type HelloRequest = { 158 | name: string; 159 | }; 160 | 161 | export type HelloReply = { 162 | message: string; 163 | }; 164 | 165 | @service("services.ExampleService") 166 | export default class ExampleService { 167 | async sayHello(req: HelloRequest): Promise { 168 | return await Promise.resolve({ message: "Hello, " + req.name }); 169 | } 170 | } 171 | ` 172 | 173 | var scriptGoTpl = `package main 174 | 175 | import ( 176 | "context" 177 | "fmt" 178 | 179 | "github.com/ayonli/goext" 180 | "github.com/ayonli/ngrpc" 181 | "github.com/ayonli/ngrpc/services" 182 | "github.com/ayonli/ngrpc/services/proto" 183 | ) 184 | 185 | func main() { 186 | done := ngrpc.ForSnippet() 187 | defer done() 188 | 189 | ctx := context.Background() 190 | expSrv := goext.Ok((&services.ExampleService{}).GetClient("")) 191 | 192 | reply := goext.Ok(expSrv.SayHello(ctx, &proto.HelloRequest{Name: "World"})) 193 | fmt.Println(reply.Message) 194 | } 195 | ` 196 | 197 | var scriptTsTpl = `import ngrpc from "@ayonli/ngrpc"; 198 | 199 | ngrpc.runSnippet(async () => { 200 | const reply = await services.ExampleService.sayHello({ name: "World" }); 201 | console.log(reply.message); 202 | }); 203 | ` 204 | 205 | var initCmd = &cobra.Command{ 206 | Use: "init", 207 | Short: "initiate a new NgRPC project", 208 | Run: func(cmd *cobra.Command, args []string) { 209 | tsConfFile := "tsconfig.json" 210 | confFile := "ngrpc.json" 211 | protoDir := "proto" 212 | protoFile := "proto/ExampleService.proto" 213 | servicesDir := "services" 214 | scriptsDir := "scripts" 215 | 216 | var goModName = getGoModuleName() 217 | var entryFile string 218 | var serviceFile string 219 | var scriptFile string 220 | template := cmd.Flag("template").Value.String() 221 | 222 | if template == "go" { 223 | if goModName == "" { 224 | fmt.Println("'go.mod' file not found in the current directory") 225 | return 226 | } else { 227 | entryFile = "main.go" 228 | serviceFile = "services/ExampleService.go" 229 | scriptFile = "scripts/main.go" 230 | } 231 | } else if template == "node" { 232 | if !util.Exists("package.json") { 233 | fmt.Println("'package.json' file not found in the current directory") 234 | return 235 | } else { 236 | entryFile = "main.ts" 237 | serviceFile = "services/ExampleService.ts" 238 | scriptFile = "scripts/main.ts" 239 | } 240 | } else { 241 | fmt.Printf("template '%s' is not supported\n", template) 242 | return 243 | } 244 | 245 | if template == "node" { 246 | if util.Exists(tsConfFile) { 247 | fmt.Printf("file '%s' already exists\n", tsConfFile) 248 | } else { 249 | os.WriteFile(tsConfFile, []byte(tsConfTpl), 0644) 250 | fmt.Printf("tsconfig file written to '%s'\n", tsConfFile) 251 | } 252 | } 253 | 254 | if util.Exists(confFile) { 255 | fmt.Printf("file '%s' already exists\n", confFile) 256 | } else { 257 | var tpl string 258 | 259 | if template == "go" { 260 | tpl = confTpl 261 | } else if template == "node" { 262 | tpl = strings.Replace(confTpl, `"main.go"`, `"main.ts"`, -1) 263 | } 264 | 265 | os.WriteFile(confFile, []byte(tpl), 0644) 266 | fmt.Printf("config file written to '%s'\n", confFile) 267 | } 268 | 269 | if util.Exists(entryFile) { 270 | fmt.Printf("entry file '%s' already exists\n", entryFile) 271 | } else { 272 | var tpl string 273 | 274 | if template == "go" { 275 | tpl = strings.Replace( 276 | entryGoTpl, 277 | "github.com/ayonli/ngrpc/services", 278 | goModName+"/services", 279 | -1) 280 | } else if template == "node" { 281 | tpl = entryTsTpl 282 | } 283 | 284 | os.WriteFile(entryFile, []byte(tpl), 0644) 285 | fmt.Printf("entry file written to '%s'\n", entryFile) 286 | } 287 | 288 | if util.Exists(protoDir) { 289 | fmt.Printf("path '%s' already exists\n", protoDir) 290 | } else { 291 | util.EnsureDir(protoDir) 292 | fmt.Printf("path '%s' created\n", protoDir) 293 | } 294 | 295 | if util.Exists(servicesDir) { 296 | fmt.Printf("path '%s' already exists\n", servicesDir) 297 | } else { 298 | util.EnsureDir(servicesDir) 299 | fmt.Printf("path '%s' created\n", servicesDir) 300 | } 301 | 302 | if util.Exists(scriptsDir) { 303 | fmt.Printf("path '%s' already exists\n", scriptsDir) 304 | } else { 305 | util.EnsureDir(scriptsDir) 306 | fmt.Printf("path '%s' created\n", scriptsDir) 307 | } 308 | 309 | if util.Exists(protoFile) { 310 | fmt.Printf("file '%s' already exists\n", protoFile) 311 | } else { 312 | os.WriteFile(protoFile, []byte(exampleProtoTpl), 0644) 313 | fmt.Printf("example proto file written to '%s'\n", protoFile) 314 | } 315 | 316 | if util.Exists(serviceFile) { 317 | fmt.Printf("file '%s' already exists\n", serviceFile) 318 | } else { 319 | var tpl string 320 | 321 | if template == "go" { 322 | tpl = strings.Replace( 323 | exampleServiceGoTpl, 324 | "github.com/ayonli/ngrpc/services", 325 | goModName+"/services", 326 | -1) 327 | } else if template == "node" { 328 | tpl = exampleServiceTsTpl 329 | } 330 | 331 | os.WriteFile(serviceFile, []byte(tpl), 0644) 332 | fmt.Printf("example service file written to '%s'\n", serviceFile) 333 | } 334 | 335 | if util.Exists(scriptFile) { 336 | fmt.Printf("file '%s' already exists\n", scriptFile) 337 | } else { 338 | var tpl string 339 | 340 | if template == "go" { 341 | tpl = strings.Replace( 342 | scriptGoTpl, 343 | "github.com/ayonli/ngrpc/services", 344 | goModName+"/services", 345 | -1) 346 | } else if template == "node" { 347 | tpl = scriptTsTpl 348 | } 349 | 350 | os.WriteFile(scriptFile, []byte(tpl), 0644) 351 | fmt.Printf("script file written to '%s'\n", scriptFile) 352 | } 353 | 354 | // install dependencies 355 | var depCmd *exec.Cmd 356 | 357 | if template == "go" && !goModuleExists("github.com/ayonli/ngrpc") { 358 | depCmd = exec.Command("go", "get", "-u", "github.com/ayonli/ngrpc") 359 | } else if template == "node" && !nodeModuleExists("@ayonli/ngrpc") { 360 | depCmd = exec.Command("npm", "i", "@ayonli/ngrpc") 361 | } 362 | 363 | if depCmd != nil { 364 | depCmd.Stdout = os.Stdout 365 | depCmd.Stderr = os.Stderr 366 | err := depCmd.Run() 367 | 368 | if err != nil { 369 | fmt.Println(err) 370 | return 371 | } else { 372 | cmd = nil 373 | } 374 | } 375 | 376 | if template == "go" { 377 | protoc() // generate code from proto files 378 | 379 | depCmd = exec.Command("go", "mod", "tidy") 380 | } else if template == "node" { 381 | depCmd = exec.Command( 382 | "npm", 383 | "i", 384 | "-D", 385 | "@types/node", 386 | "typescript", 387 | "tslib", 388 | "source-map-support") 389 | } 390 | 391 | if depCmd != nil { 392 | depCmd.Stdout = os.Stdout 393 | depCmd.Stderr = os.Stderr 394 | err := depCmd.Run() 395 | 396 | if err != nil { 397 | fmt.Println(err) 398 | return 399 | } else { 400 | cmd = nil 401 | } 402 | } 403 | 404 | fmt.Println("") 405 | fmt.Println("All procedures finished, now try the following command to start your first gRPC app") 406 | fmt.Println("") 407 | fmt.Println(" ngrpc start") 408 | fmt.Println("") 409 | fmt.Println("Then try the following command to check out all the running apps") 410 | fmt.Println("") 411 | fmt.Println(" ngrpc list") 412 | fmt.Println("") 413 | fmt.Println("Or try the following command to run a script that attaches to the service and get some results") 414 | fmt.Println("") 415 | 416 | if template == "go" { 417 | fmt.Println(" ngrpc run scripts/main.go") 418 | } else if template == "node" { 419 | fmt.Println(" ngrpc run scripts/main.ts") 420 | } 421 | 422 | fmt.Println("") 423 | }, 424 | } 425 | 426 | func init() { 427 | rootCmd.AddCommand(initCmd) 428 | initCmd.Flags().StringP("template", "t", "", `available values are "go" or "node"`) 429 | } 430 | 431 | func getGoModuleName() string { 432 | data, err := os.ReadFile("go.mod") 433 | 434 | if err == nil { 435 | match := stringx.Match(string(data), `module (\S+)`) 436 | 437 | if match != nil { 438 | return match[1] 439 | } 440 | } 441 | 442 | return "" 443 | } 444 | 445 | func goModuleExists(modName string) bool { 446 | cmd := exec.Command("go", "list", modName) 447 | _, err := cmd.Output() 448 | 449 | return err == nil 450 | } 451 | 452 | func nodeModuleExists(modName string) bool { 453 | cmd := exec.Command("npm", "ls", modName) 454 | _, err := cmd.Output() 455 | 456 | return err == nil 457 | } 458 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ayonli/ngrpc/pm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var listCmd = &cobra.Command{ 9 | Use: "list", 10 | Aliases: []string{"ls"}, 11 | Short: "list all apps (exclude non-served ones)", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | pm.SendCommand("list", "") 14 | }, 15 | } 16 | 17 | func init() { 18 | rootCmd.AddCommand(listCmd) 19 | } 20 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/protoc.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/ayonli/goext" 11 | "github.com/ayonli/ngrpc/config" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var protocCmd = &cobra.Command{ 16 | Use: "protoc", 17 | Short: "generate golang program files from the proto files", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | protoc() 20 | }, 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(protocCmd) 25 | } 26 | 27 | func ensureDeps() { 28 | dep1 := "google.golang.org/protobuf/cmd/protoc-gen-go" 29 | dep2 := "google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3" 30 | 31 | if !goModuleExists(dep1) { 32 | goext.Ok(0, exec.Command("go", "install", dep1).Run()) 33 | goext.Ok(0, exec.Command("go", "install", dep2).Run()) 34 | } 35 | } 36 | 37 | func protoc() { 38 | if getGoModuleName() == "" { 39 | fmt.Println("the current directory is not a go module") 40 | return 41 | } 42 | 43 | _, err := exec.LookPath("protoc") 44 | 45 | if err != nil { 46 | fmt.Println(err) 47 | return 48 | } 49 | 50 | ensureDeps() 51 | 52 | conf := goext.Ok(config.LoadConfig()) 53 | protoFileRecords := map[string][]string{} 54 | 55 | if len(conf.ProtoPaths) > 0 { 56 | for _, dir := range conf.ProtoPaths { 57 | filenames := scanProtoFilenames(dir) 58 | 59 | if len(filenames) > 0 { 60 | protoFileRecords[dir] = filenames 61 | } 62 | } 63 | } else { 64 | filenames := scanProtoFilenames("proto") 65 | 66 | if len(filenames) > 0 { 67 | protoFileRecords["proto"] = filenames 68 | } 69 | } 70 | 71 | if len(protoFileRecords) == 0 { 72 | fmt.Println("no proto files have been found") 73 | return 74 | } 75 | 76 | for protoPath, filenames := range protoFileRecords { 77 | for _, filename := range filenames { 78 | fmt.Printf("generate code for '%s'\n", filename) 79 | genGoCode(protoPath, conf.ImportRoot, filename) 80 | } 81 | } 82 | } 83 | 84 | func scanProtoFilenames(dir string) []string { 85 | filenames := []string{} 86 | 87 | files := goext.Ok(os.ReadDir(dir)) 88 | protoFiles := []string{} 89 | subDirs := []string{} 90 | 91 | for _, file := range files { 92 | basename := file.Name() 93 | filename := filepath.Join(dir, basename) 94 | 95 | if file.IsDir() { 96 | subDirs = append(subDirs, filename) 97 | } else if strings.HasSuffix(basename, ".proto") { 98 | protoFiles = append(protoFiles, filename) 99 | } 100 | } 101 | 102 | if len(protoFiles) > 0 { 103 | filenames = append(filenames, protoFiles...) 104 | } 105 | 106 | if len(subDirs) > 0 { 107 | for _, subDir := range subDirs { 108 | filenames = append(filenames, scanProtoFilenames(subDir)...) 109 | } 110 | } 111 | 112 | return filenames 113 | } 114 | 115 | func genGoCode(protoPath string, importRoot string, filename string) { 116 | cmd := exec.Command("protoc", 117 | "--proto_path="+protoPath, 118 | "--go_out=./"+filepath.Join(importRoot, "services"), 119 | "--go-grpc_out=./"+filepath.Join(importRoot, "services"), 120 | filename) 121 | 122 | cmd.Stdout = os.Stdout 123 | cmd.Stderr = os.Stderr 124 | 125 | cmd.Run() 126 | } 127 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/reload.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ayonli/ngrpc/pm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var reloadCmd = &cobra.Command{ 9 | Use: "reload [app]", 10 | Short: "hot-reload an app or all apps", 11 | Run: func(cmd *cobra.Command, args []string) { 12 | if len(args) > 0 { 13 | pm.SendCommand("reload", args[0]) 14 | } else { 15 | pm.SendCommand("reload", "") 16 | } 17 | }, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(reloadCmd) 22 | } 23 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/restart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ayonli/ngrpc/pm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var restartCmd = &cobra.Command{ 9 | Use: "restart [app]", 10 | Short: "restart an app or all apps", 11 | Run: func(cmd *cobra.Command, args []string) { 12 | if len(args) > 0 { 13 | pm.SendCommand("restart", args[0]) 14 | } else { 15 | pm.SendCommand("restart", "") 16 | } 17 | }, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(restartCmd) 22 | } 23 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var version string 10 | 11 | // rootCmd represents the base command when called without any subcommands 12 | var rootCmd = &cobra.Command{ 13 | Use: "ngrpc", 14 | Version: version, 15 | Short: "Easily manage NgRPC apps", 16 | } 17 | 18 | // Execute adds all child commands to the root command and sets flags appropriately. 19 | // This is called by main.main(). It only needs to happen once to the rootCmd. 20 | func Execute() { 21 | err := rootCmd.Execute() 22 | if err != nil { 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func init() { 28 | rootCmd.AddCommand(&cobra.Command{ 29 | Use: "completion", 30 | Short: "Generate the autocompletion script for the specified shell", 31 | Hidden: true, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | 9 | "github.com/ayonli/goext" 10 | "github.com/ayonli/ngrpc/config" 11 | "github.com/ayonli/ngrpc/pm" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var runCmd = &cobra.Command{ 16 | Use: "run [args...]", 17 | Short: "run a script program", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | if len(args) == 0 { 20 | fmt.Println("filename must be provided") 21 | return 22 | } 23 | 24 | filename := args[0] 25 | ext := filepath.Ext(filename) 26 | env := map[string]string{} 27 | var script *exec.Cmd 28 | 29 | if ext == ".go" { 30 | script = exec.Command("go", "run", filename) 31 | } else if ext == ".ts" { 32 | cfg := goext.Ok(config.LoadConfig()) 33 | tsCfg := goext.Ok(config.LoadTsConfig(cfg.Tsconfig)) 34 | outDir, outFile := pm.ResolveTsEntry(filename, tsCfg) 35 | goext.Ok(0, pm.CompileTs(tsCfg, outDir)) 36 | script = exec.Command("node", outFile) 37 | env["IMPORT_ROOT"] = outDir 38 | } else if ext == ".js" { 39 | script = exec.Command("node", filename) 40 | } 41 | 42 | if len(args) > 1 { 43 | script.Args = append(script.Args, args[1:]...) 44 | } 45 | 46 | script.Stdin = os.Stdin 47 | script.Stdout = os.Stdout 48 | script.Stderr = os.Stderr 49 | 50 | if len(env) > 0 { 51 | script.Env = os.Environ() 52 | 53 | for key, value := range env { 54 | script.Env = append(script.Env, key+"="+value) 55 | } 56 | } 57 | 58 | script.Run() 59 | }, 60 | } 61 | 62 | func init() { 63 | rootCmd.AddCommand(runCmd) 64 | } 65 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/start.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ayonli/ngrpc/pm" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var startCmd = &cobra.Command{ 11 | Use: "start [app]", 12 | Short: "start an app or all apps (exclude non-served ones)", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | if !pm.IsHostOnline() { 15 | err := startHost(false) 16 | 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | } 22 | 23 | if len(args) > 0 { 24 | pm.SendCommand("start", args[0]) 25 | } else { 26 | pm.SendCommand("start", "") 27 | } 28 | }, 29 | } 30 | 31 | func init() { 32 | rootCmd.AddCommand(startCmd) 33 | } 34 | -------------------------------------------------------------------------------- /cli/ngrpc/cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/ayonli/ngrpc/pm" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var stopCmd = &cobra.Command{ 9 | Use: "stop [app]", 10 | Short: "stop an app or all apps", 11 | Run: func(cmd *cobra.Command, args []string) { 12 | if len(args) > 0 { 13 | pm.SendCommand("stop", args[0]) 14 | } else { 15 | pm.SendCommand("stop", "") 16 | } 17 | }, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(stopCmd) 22 | } 23 | -------------------------------------------------------------------------------- /cli/ngrpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ayonli/ngrpc/cli/ngrpc/cmd" 8 | "github.com/ayonli/ngrpc/config" 9 | "github.com/ayonli/ngrpc/pm" 10 | _ "github.com/spf13/cobra" 11 | ) 12 | 13 | func main() { 14 | args := os.Args 15 | 16 | if len(args) > 1 && args[1] == "host-server" { 17 | config, err := config.LoadConfig() 18 | 19 | if err != nil { 20 | fmt.Println(err) 21 | return 22 | } 23 | 24 | standalone := len(args) > 2 && args[2] == "--standalone" 25 | host := pm.NewHost(config, standalone) 26 | err = host.Start(true) 27 | 28 | if err != nil { 29 | fmt.Println(err) 30 | } 31 | } else { 32 | cmd.Execute() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cli_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package ngrpc_test 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/ayonli/goext" 15 | "github.com/ayonli/goext/stringx" 16 | "github.com/ayonli/ngrpc" 17 | "github.com/ayonli/ngrpc/services" 18 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 19 | "github.com/ayonli/ngrpc/services/proto" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestHostCommand(t *testing.T) { 24 | output := goext.Ok(exec.Command("ngrpc", "host").Output()) 25 | assert.Contains(t, string(output), "host server started") 26 | 27 | exam := goext.Ok(exec.Command("ps", "aux").Output()) 28 | assert.Contains(t, string(exam), "ngrpc host-server --standalone") 29 | 30 | output = goext.Ok(exec.Command("ngrpc", "host", "--stop").Output()) 31 | assert.Contains(t, string(output), "host server shut down") 32 | 33 | time.Sleep(time.Millisecond * 10) 34 | exam = goext.Ok(exec.Command("ps", "aux").Output()) 35 | assert.NotContains(t, string(exam), "ngrpc host-server --standalone") 36 | } 37 | 38 | func TestStartAndStopCommand_singleApp(t *testing.T) { 39 | output := goext.Ok(exec.Command("ngrpc", "start", "example-server").Output()) 40 | assert.Contains(t, string(output), "app [example-server] started") 41 | 42 | done := ngrpc.ForSnippet() 43 | defer done() 44 | 45 | ctx := context.Background() 46 | srv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 47 | reply := goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 48 | assert.Equal(t, "Hello, World", reply.Message) 49 | 50 | output = goext.Ok(exec.Command("ngrpc", "stop", "example-server").Output()) 51 | assert.Contains(t, string(output), "app [example-server] stopped") 52 | 53 | exam := goext.Ok(exec.Command("ps", "aux").Output()) 54 | assert.NotContains(t, string(exam), "example-server") 55 | 56 | goext.Ok(0, exec.Command("ngrpc", "host", "--stop").Run()) 57 | time.Sleep(time.Millisecond * 10) 58 | } 59 | 60 | func TestStartAndStopCommand_allApps(t *testing.T) { 61 | output := goext.Ok(exec.Command("ngrpc", "start").Output()) 62 | assert.Contains(t, string(output), "host server started") 63 | assert.Contains(t, string(output), "app [example-server] started") 64 | assert.Contains(t, string(output), "app [user-server] started") 65 | assert.Contains(t, string(output), "app [post-server] started") 66 | 67 | done := ngrpc.ForSnippet() 68 | defer done() 69 | 70 | ctx := context.Background() 71 | exampleSrv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 72 | reply := goext.Ok((exampleSrv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 73 | assert.Equal(t, "Hello, World", reply.Message) 74 | 75 | userId := "ayon.li" 76 | userSrv := goext.Ok(ngrpc.GetServiceClient(&services.UserService{}, userId)) 77 | user := goext.Ok(userSrv.GetUser(ctx, &services_proto.UserQuery{Id: &userId})) 78 | assert.Equal(t, "A-yon Lee", user.Name) 79 | 80 | postSrv := goext.Ok(ngrpc.GetServiceClient(&services.PostService{}, userId)) 81 | posts := goext.Ok(postSrv.SearchPosts(ctx, &services_proto.PostsQuery{Author: &userId})) 82 | assert.True(t, len(posts.Posts) > 0) 83 | 84 | output = goext.Ok(exec.Command("ngrpc", "stop").Output()) 85 | assert.Contains(t, string(output), "app [example-server] stopped") 86 | assert.Contains(t, string(output), "app [user-server] stopped") 87 | assert.Contains(t, string(output), "app [post-server] stopped") 88 | assert.Contains(t, string(output), "host server shut down") 89 | 90 | time.Sleep(time.Millisecond * 10) // for system to release resources 91 | exam := goext.Ok(exec.Command("ps", "aux").Output()) 92 | assert.NotContains(t, string(exam), "example-server") 93 | assert.NotContains(t, string(exam), "user-server") 94 | assert.NotContains(t, string(exam), "post-server") 95 | } 96 | 97 | func TestListCommand(t *testing.T) { 98 | goext.Ok(0, exec.Command("ngrpc", "start").Run()) 99 | output := goext.Ok(exec.Command("ngrpc", "list").Output()) 100 | rows := strings.Split(string(output), "\n") 101 | 102 | for i, row := range rows { 103 | columns := strings.Fields(row) 104 | 105 | if i == 0 { 106 | assert.Equal(t, "App", columns[0]) 107 | assert.Equal(t, "URL", columns[1]) 108 | assert.Equal(t, "Status", columns[2]) 109 | assert.Equal(t, "Pid", columns[3]) 110 | assert.Equal(t, "Uptime", columns[4]) 111 | assert.Equal(t, "Memory", columns[5]) 112 | assert.Equal(t, "CPU", columns[6]) 113 | } else if i == 1 { 114 | assert.Equal(t, "example-server", columns[0]) 115 | assert.Equal(t, "grpc://localhost:4000", columns[1]) 116 | assert.Equal(t, "running", columns[2]) 117 | assert.NotNil(t, stringx.Match(columns[3], "^\\d+$")) 118 | assert.NotNil(t, stringx.Match(columns[4], "^\\ds$")) 119 | } else if i == 2 { 120 | assert.Equal(t, "user-server", columns[0]) 121 | assert.Equal(t, "grpcs://localhost:4001", columns[1]) 122 | assert.Equal(t, "running", columns[2]) 123 | assert.NotNil(t, stringx.Match(columns[3], "^\\d+$")) 124 | assert.NotNil(t, stringx.Match(columns[4], "^\\ds$")) 125 | } else if i == 3 { 126 | assert.Equal(t, "post-server", columns[0]) 127 | assert.Equal(t, "grpcs://localhost:4002", columns[1]) 128 | assert.Equal(t, "running", columns[2]) 129 | assert.NotNil(t, stringx.Match(columns[3], "^\\d+$")) 130 | assert.NotNil(t, stringx.Match(columns[4], "^\\ds$")) 131 | } 132 | } 133 | 134 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 135 | time.Sleep(time.Millisecond * 10) 136 | 137 | output = goext.Ok(exec.Command("ngrpc", "list").Output()) 138 | rows = strings.Split(string(output), "\n") 139 | 140 | for i, row := range rows { 141 | columns := strings.Fields(row) 142 | 143 | if i == 0 { 144 | assert.Equal(t, "App", columns[0]) 145 | assert.Equal(t, "URL", columns[1]) 146 | assert.Equal(t, "Status", columns[2]) 147 | assert.Equal(t, "Pid", columns[3]) 148 | assert.Equal(t, "Uptime", columns[4]) 149 | assert.Equal(t, "Memory", columns[5]) 150 | assert.Equal(t, "CPU", columns[6]) 151 | } else if i == 1 { 152 | assert.Equal(t, "example-server", columns[0]) 153 | assert.Equal(t, "grpc://localhost:4000", columns[1]) 154 | assert.Equal(t, "stopped", columns[2]) 155 | assert.Equal(t, "N/A", columns[3]) 156 | assert.Equal(t, "N/A", columns[4]) 157 | assert.Equal(t, "N/A", columns[5]) 158 | assert.Equal(t, "N/A", columns[6]) 159 | } else if i == 2 { 160 | assert.Equal(t, "user-server", columns[0]) 161 | assert.Equal(t, "grpcs://localhost:4001", columns[1]) 162 | assert.Equal(t, "stopped", columns[2]) 163 | assert.Equal(t, "N/A", columns[3]) 164 | assert.Equal(t, "N/A", columns[4]) 165 | assert.Equal(t, "N/A", columns[5]) 166 | assert.Equal(t, "N/A", columns[6]) 167 | } else if i == 3 { 168 | assert.Equal(t, "post-server", columns[0]) 169 | assert.Equal(t, "grpcs://localhost:4002", columns[1]) 170 | assert.Equal(t, "stopped", columns[2]) 171 | assert.Equal(t, "N/A", columns[3]) 172 | assert.Equal(t, "N/A", columns[4]) 173 | assert.Equal(t, "N/A", columns[5]) 174 | assert.Equal(t, "N/A", columns[6]) 175 | } 176 | } 177 | } 178 | 179 | func TestReloadCommand_singleApp(t *testing.T) { 180 | goext.Ok(0, exec.Command("ngrpc", "start", "example-server").Run()) 181 | 182 | done := ngrpc.ForSnippet() 183 | defer done() 184 | 185 | ctx := context.Background() 186 | srv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 187 | reply := goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 188 | assert.Equal(t, "Hello, World", reply.Message) 189 | 190 | oldContents := string(goext.Ok(os.ReadFile("services/ExampleService.ts"))) 191 | newContents := strings.Replace(oldContents, `"Hello, "`, `"Hi, "`, 1) 192 | goext.Ok(0, os.WriteFile("services/ExampleService.ts", []byte(newContents), 0644)) 193 | defer os.WriteFile("services/ExampleService.ts", []byte(oldContents), 0644) 194 | 195 | output := goext.Ok(exec.Command("ngrpc", "reload", "example-server").Output()) 196 | assert.Contains(t, string(output), "app [example-server] hot-reloaded") 197 | 198 | reply = goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 199 | assert.Equal(t, "Hi, World", reply.Message) 200 | 201 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 202 | time.Sleep(time.Millisecond * 10) 203 | } 204 | 205 | func TestReloadCommand_allApps(t *testing.T) { 206 | goext.Ok(0, exec.Command("ngrpc", "start").Run()) 207 | 208 | done := ngrpc.ForSnippet() 209 | defer done() 210 | 211 | ctx := context.Background() 212 | srv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 213 | reply := goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 214 | assert.Equal(t, "Hello, World", reply.Message) 215 | 216 | oldContents := string(goext.Ok(os.ReadFile("services/ExampleService.ts"))) 217 | newContents := strings.Replace(oldContents, `"Hello, "`, `"Hi, "`, 1) 218 | goext.Ok(0, os.WriteFile("services/ExampleService.ts", []byte(newContents), 0644)) 219 | defer os.WriteFile("services/ExampleService.ts", []byte(oldContents), 0644) 220 | 221 | output := goext.Ok(exec.Command("ngrpc", "reload").Output()) 222 | assert.Contains(t, string(output), "app [example-server] hot-reloaded") 223 | assert.Contains(t, string(output), "app [post-server] hot-reloaded") 224 | assert.Contains(t, string(output), "app [user-server] does not support hot-reloading") 225 | 226 | reply = goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 227 | assert.Equal(t, "Hi, World", reply.Message) 228 | 229 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 230 | time.Sleep(time.Millisecond * 10) 231 | } 232 | 233 | func TestRestartCommand_singleApp(t *testing.T) { 234 | goext.Ok(0, exec.Command("ngrpc", "start", "example-server").Run()) 235 | 236 | done := ngrpc.ForSnippet() 237 | defer done() 238 | 239 | ctx := context.Background() 240 | srv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 241 | reply := goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 242 | assert.Equal(t, "Hello, World", reply.Message) 243 | 244 | oldContents := string(goext.Ok(os.ReadFile("services/ExampleService.ts"))) 245 | newContents := strings.Replace(oldContents, `"Hello, "`, `"Hi, "`, 1) 246 | goext.Ok(0, os.WriteFile("services/ExampleService.ts", []byte(newContents), 0644)) 247 | defer os.WriteFile("services/ExampleService.ts", []byte(oldContents), 0644) 248 | 249 | output := goext.Ok(exec.Command("ngrpc", "restart", "example-server").Output()) 250 | assert.Contains(t, string(output), "app [example-server] stopped") 251 | assert.Contains(t, string(output), "app [example-server] started") 252 | 253 | reply = goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 254 | assert.Equal(t, "Hi, World", reply.Message) 255 | 256 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 257 | time.Sleep(time.Millisecond * 10) 258 | } 259 | 260 | func TestRestartCommand_allApps(t *testing.T) { 261 | goext.Ok(0, exec.Command("ngrpc", "start").Run()) 262 | 263 | done := ngrpc.ForSnippet() 264 | defer done() 265 | 266 | ctx := context.Background() 267 | srv := goext.Ok(ngrpc.GetServiceClient(&services.ExampleService{}, "")) 268 | reply := goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 269 | assert.Equal(t, "Hello, World", reply.Message) 270 | 271 | oldContents := string(goext.Ok(os.ReadFile("services/ExampleService.ts"))) 272 | newContents := strings.Replace(oldContents, `"Hello, "`, `"Hi, "`, 1) 273 | goext.Ok(0, os.WriteFile("services/ExampleService.ts", []byte(newContents), 0644)) 274 | defer os.WriteFile("services/ExampleService.ts", []byte(oldContents), 0644) 275 | 276 | output := goext.Ok(exec.Command("ngrpc", "restart").Output()) 277 | assert.Contains(t, string(output), "app [example-server] stopped") 278 | assert.Contains(t, string(output), "app [example-server] started") 279 | assert.Contains(t, string(output), "app [user-server] stopped") 280 | assert.Contains(t, string(output), "app [user-server] started") 281 | assert.Contains(t, string(output), "app [post-server] stopped") 282 | assert.Contains(t, string(output), "app [post-server] started") 283 | 284 | reply = goext.Ok((srv.SayHello(ctx, &proto.HelloRequest{Name: "World"}))) 285 | assert.Equal(t, "Hi, World", reply.Message) 286 | 287 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 288 | time.Sleep(time.Millisecond * 10) 289 | } 290 | 291 | func TestRunCommand_go(t *testing.T) { 292 | goext.Ok(0, exec.Command("ngrpc", "start").Run()) 293 | 294 | output := goext.Ok(exec.Command("ngrpc", "run", "scripts/main.go").Output()) 295 | assert.Contains(t, string(output), "Hello, World") 296 | 297 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 298 | time.Sleep(time.Millisecond * 10) 299 | } 300 | 301 | func TestRunCommand_ts(t *testing.T) { 302 | goext.Ok(0, exec.Command("ngrpc", "start").Run()) 303 | 304 | output := goext.Ok(exec.Command("ngrpc", "run", "scripts/main.ts").Output()) 305 | assert.Contains(t, string(output), "Hello, World") 306 | 307 | goext.Ok(0, exec.Command("ngrpc", "stop").Run()) 308 | time.Sleep(time.Millisecond * 10) 309 | } 310 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/json" 7 | "fmt" 8 | "net/url" 9 | "os" 10 | 11 | "github.com/ayonli/goext" 12 | "github.com/ayonli/ngrpc/util" 13 | "github.com/tidwall/jsonc" 14 | "google.golang.org/grpc/credentials" 15 | "google.golang.org/grpc/credentials/insecure" 16 | ) 17 | 18 | // App is used both to configure the apps. 19 | type App struct { 20 | // The name of the app. 21 | Name string `json:"name"` 22 | // The URL of the gRPC server, supported protocols are `grpc:`, `grpcs:`, `http:`, `https:` or 23 | // `xds:`. 24 | Url string `json:"url"` 25 | // If this app can be served by as the gRPC server. 26 | Serve bool `json:"serve"` 27 | // The services served by this app. 28 | Services []string `json:"services"` 29 | // The certificate filename when using TLS/SSL. 30 | Cert string `json:"cert"` 31 | // The private key filename when using TLS/SSL. 32 | Key string `json:"key"` 33 | // The CA filename used to verify the other peer's certificates, when omitted, the system's root 34 | // CAs will be used. 35 | // 36 | // It's recommended that the gRPC application uses a self-signed certificate with a non-public 37 | // CA, so the client and the server can establish a private connection that no outsiders can 38 | // join. 39 | Ca string `json:"ca"` 40 | Stdout string `json:"stdout"` 41 | Stderr string `json:"stderr"` 42 | Entry string `json:"entry"` 43 | Env map[string]string `json:"env"` 44 | } 45 | 46 | // Config is used to store configurations of the apps. 47 | type Config struct { 48 | Tsconfig string `json:"tsconfig"` 49 | // Deprecated: use `App.Entry` instead. 50 | Entry string `json:"entry"` 51 | ImportRoot string `json:"importRoot"` 52 | ProtoPaths []string `json:"protoPaths"` 53 | Apps []App `json:"apps"` 54 | } 55 | 56 | func LoadConfig() (Config, error) { 57 | var cfg *Config 58 | defaultFile := util.AbsPath("ngrpc.json", false) 59 | localFile := util.AbsPath("ngrpc.local.json", false) 60 | 61 | if util.Exists(localFile) { 62 | data, err := os.ReadFile(localFile) 63 | 64 | if err == nil { 65 | json.Unmarshal(jsonc.ToJSON(data), &cfg) 66 | } 67 | } 68 | 69 | if cfg == nil && util.Exists(defaultFile) { 70 | data, err := os.ReadFile(defaultFile) 71 | 72 | if err == nil { 73 | err = json.Unmarshal(jsonc.ToJSON(data), &cfg) 74 | } 75 | 76 | if err != nil { 77 | return *cfg, err 78 | } 79 | } 80 | 81 | if cfg != nil && len(cfg.Apps) > 0 { 82 | apps := []App{} 83 | 84 | for _, app := range cfg.Apps { 85 | if app.Entry == "" && cfg.Entry != "" { 86 | app.Entry = cfg.Entry 87 | } 88 | 89 | apps = append(apps, app) 90 | } 91 | 92 | cfg.Apps = apps 93 | 94 | return *cfg, nil 95 | } else { 96 | return Config{}, fmt.Errorf("unable to load config file: %v", defaultFile) 97 | } 98 | } 99 | 100 | func GetAddress(urlObj *url.URL) string { 101 | addr := urlObj.Hostname() 102 | 103 | if urlObj.Scheme == "grpcs" || urlObj.Scheme == "https" { 104 | if urlObj.Port() == "" { 105 | addr += ":443" // Use port 443 by default for secure protocols. 106 | } else { 107 | addr += ":" + urlObj.Port() 108 | } 109 | } else if (urlObj.Scheme == "grpc" || urlObj.Scheme == "http") && urlObj.Port() == "" { 110 | addr += ":80" // Use port 80 by default for insecure protocols. 111 | } else { 112 | addr += ":" + urlObj.Port() 113 | } 114 | 115 | return addr 116 | } 117 | 118 | func GetCredentials(app App, urlObj *url.URL) (credentials.TransportCredentials, error) { 119 | // Create secure (SSL/TLS) credentials, use x509 standard. 120 | var createSecure = (func(args ...any) (credentials.TransportCredentials, error) { 121 | return goext.Try(func() credentials.TransportCredentials { 122 | cert := goext.Ok(tls.LoadX509KeyPair(app.Cert, app.Key)) 123 | var certPool *x509.CertPool 124 | 125 | if app.Ca != "" { 126 | certPool = x509.NewCertPool() 127 | ca := goext.Ok(os.ReadFile(app.Ca)) 128 | 129 | if ok := certPool.AppendCertsFromPEM(ca); !ok { 130 | panic(fmt.Errorf("unable to create cert pool for CA: %v", app.Ca)) 131 | } 132 | } 133 | 134 | return credentials.NewTLS(&tls.Config{ 135 | Certificates: []tls.Certificate{cert}, 136 | RootCAs: certPool, 137 | }) 138 | }) 139 | }) 140 | 141 | if urlObj.Scheme == "grpcs" || urlObj.Scheme == "https" { 142 | if app.Cert == "" { 143 | return nil, fmt.Errorf("missing 'Cert' config for app [%s]", app.Name) 144 | } else if app.Key == "" { 145 | return nil, fmt.Errorf("missing 'Key' config for app [%s]", app.Name) 146 | } else { 147 | return createSecure() 148 | } 149 | } else if app.Cert != "" && app.Key != "" { 150 | return createSecure() 151 | } else { 152 | // Create insure credentials if no certificates are set. 153 | return insecure.NewCredentials(), nil 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/ayonli/goext" 10 | "github.com/ayonli/ngrpc/util" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestLoadConfigFile(t *testing.T) { 15 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 16 | defer os.Remove("ngrpc.json") 17 | 18 | config := goext.Ok(LoadConfig()) 19 | assert.True(t, len(config.Apps) > 0) 20 | } 21 | 22 | func TestLoadLocalConfigFile(t *testing.T) { 23 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.local.json")) 24 | defer os.Remove("ngrpc.local.json") 25 | 26 | config := goext.Ok(LoadConfig()) 27 | assert.True(t, len(config.Apps) > 0) 28 | } 29 | 30 | func TestLoadConfigFailure(t *testing.T) { 31 | cwd, _ := os.Getwd() 32 | filename := filepath.Join(cwd, "ngrpc.json") 33 | config, err := LoadConfig() 34 | 35 | assert.Equal(t, Config{Entry: "", Apps: []App(nil)}, config) 36 | assert.Equal(t, "unable to load config file: "+filename, err.Error()) 37 | } 38 | 39 | func TestGetAddress(t *testing.T) { 40 | urlObj1, _ := url.Parse("grpc://localhost:6000") 41 | urlObj2, _ := url.Parse("grpc://localhost") 42 | urlObj3, _ := url.Parse("grpcs://localhost") 43 | urlObj4, _ := url.Parse("grpcs://localhost:6000") 44 | addr1 := GetAddress(urlObj1) 45 | addr2 := GetAddress(urlObj2) 46 | addr3 := GetAddress(urlObj3) 47 | addr4 := GetAddress(urlObj4) 48 | 49 | assert.Equal(t, "localhost:6000", addr1) 50 | assert.Equal(t, "localhost:80", addr2) 51 | assert.Equal(t, "localhost:443", addr3) 52 | assert.Equal(t, "localhost:6000", addr4) 53 | } 54 | 55 | func TestGetCredentials(t *testing.T) { 56 | app1 := App{ 57 | Name: "test-server", 58 | Url: "grpc://localhost:6000", 59 | } 60 | app2 := App{ 61 | Name: "test-server", 62 | Url: "grpcs://localhost:6000", 63 | Ca: "../certs/ca.pem", 64 | Cert: "../certs/cert.pem", 65 | Key: "../certs/cert.key", 66 | } 67 | urlObj1, _ := url.Parse(app1.Url) 68 | urlObj2, _ := url.Parse(app2.Url) 69 | cred1, _ := GetCredentials(app1, urlObj1) 70 | cred2, _ := GetCredentials(app2, urlObj2) 71 | cred3, _ := GetCredentials(app2, urlObj1) 72 | 73 | assert.Equal(t, "insecure", cred1.Info().SecurityProtocol) 74 | assert.Equal(t, "tls", cred2.Info().SecurityProtocol) 75 | assert.Equal(t, "tls", cred3.Info().SecurityProtocol) 76 | } 77 | 78 | func TestGetCredentialsMissingCertFile(t *testing.T) { 79 | app := App{ 80 | Name: "server-1", 81 | Url: "grpcs://localhost:6000", 82 | } 83 | 84 | urlObj, _ := url.Parse(app.Url) 85 | _, err := GetCredentials(app, urlObj) 86 | 87 | assert.Equal(t, "missing 'Cert' config for app [server-1]", err.Error()) 88 | } 89 | 90 | func TestGetCredentialsMissingKeyFile(t *testing.T) { 91 | app := App{ 92 | Name: "server-1", 93 | Url: "grpcs://localhost:6000", 94 | Cert: "../certs/cert.pem"} 95 | 96 | urlObj, _ := url.Parse(app.Url) 97 | _, err := GetCredentials(app, urlObj) 98 | 99 | assert.Equal(t, "missing 'Key' config for app [server-1]", err.Error()) 100 | } 101 | 102 | func TestGetCredentialsInvalidCertFile(t *testing.T) { 103 | app := App{ 104 | Name: "server-1", 105 | Url: "grpcs://localhost:6000", 106 | Ca: "../certs/ca.pem", 107 | Cert: "./certs/cert.pem", 108 | Key: "./certs/cert.key", 109 | } 110 | 111 | urlObj, _ := url.Parse(app.Url) 112 | _, err := GetCredentials(app, urlObj) 113 | 114 | assert.Contains(t, err.Error(), "open ./certs/cert.pem:") 115 | } 116 | 117 | func TestGetCredentialsInvalidKeyFile(t *testing.T) { 118 | app := App{ 119 | Name: "server-1", 120 | Url: "grpcs://localhost:6000", 121 | Ca: "../certs/ca.pem", 122 | Cert: "../certs/cert.pem", 123 | Key: "./certs/cert.key", 124 | } 125 | 126 | urlObj, _ := url.Parse(app.Url) 127 | _, err := GetCredentials(app, urlObj) 128 | 129 | assert.Contains(t, err.Error(), "open ./certs/cert.key:") 130 | } 131 | 132 | func TestGetCredentialsBadCa(t *testing.T) { 133 | app := App{ 134 | Name: "server-1", 135 | Url: "grpcs://localhost:6000", 136 | Ca: "../certs/ca.srl", 137 | Cert: "../certs/cert.pem", 138 | Key: "../certs/cert.key", 139 | } 140 | 141 | urlObj, _ := url.Parse(app.Url) 142 | _, err := GetCredentials(app, urlObj) 143 | 144 | assert.Equal(t, "unable to create cert pool for CA: ../certs/ca.srl", err.Error()) 145 | } 146 | -------------------------------------------------------------------------------- /config/tsconfig.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/tidwall/jsonc" 8 | ) 9 | 10 | type CompilerOptions struct { 11 | Module string `json:"json"` 12 | Target string `json:"target"` 13 | RootDir string `json:"rootDir"` 14 | OutDir string `json:"outDir"` 15 | NoEmitOnError bool `json:"noEmitOnError"` 16 | } 17 | 18 | type TsConfig struct { 19 | CompilerOptions CompilerOptions `json:"compilerOptions"` 20 | Includes []string `json:"includes"` 21 | } 22 | 23 | func LoadTsConfig(filename string) (TsConfig, error) { 24 | var tsCfg TsConfig 25 | 26 | if filename == "" { 27 | filename = "tsconfig.json" 28 | } 29 | 30 | data, err := os.ReadFile(filename) 31 | 32 | if err != nil { 33 | return tsCfg, err 34 | } 35 | 36 | err = json.Unmarshal(jsonc.ToJSON(data), &tsCfg) 37 | 38 | return tsCfg, err 39 | } 40 | -------------------------------------------------------------------------------- /config/tsconfig_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ayonli/goext" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoadTsConfig(t *testing.T) { 11 | tsCfg := goext.Ok(LoadTsConfig("../tsconfig.json")) 12 | _, err := LoadTsConfig("") 13 | 14 | assert.NotEqual(t, "", tsCfg.CompilerOptions.Target) 15 | assert.Contains(t, err.Error(), "open tsconfig.json:") 16 | } 17 | -------------------------------------------------------------------------------- /entry/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ayonli/ngrpc" 7 | _ "github.com/ayonli/ngrpc/services" 8 | ) 9 | 10 | func main() { 11 | app, err := ngrpc.Start(ngrpc.GetAppName()) 12 | 13 | if err != nil { 14 | log.Fatal(err) 15 | } else { 16 | app.WaitForExit() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /entry/main.ts: -------------------------------------------------------------------------------- 1 | import ngrpc from "@ayonli/ngrpc"; 2 | 3 | if (require.main?.filename === __filename) { 4 | ngrpc.start(ngrpc.getAppName()).then(app => { 5 | process.send?.("ready"); // for PM2 compatibility 6 | app.waitForExit(); 7 | }).catch(err => { 8 | console.error(err); 9 | process.exit(1); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ayonli/ngrpc 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.6.1 7 | github.com/ayonli/goext v0.4.3 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/rodaine/table v1.1.0 10 | github.com/spf13/cobra v1.7.0 11 | github.com/stretchr/testify v1.8.4 12 | github.com/struCoder/pidusage v0.2.1 13 | github.com/tidwall/jsonc v0.3.2 14 | google.golang.org/grpc v1.57.0 15 | google.golang.org/protobuf v1.31.0 16 | ) 17 | 18 | require ( 19 | github.com/bytedance/sonic v1.9.1 // indirect 20 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 23 | github.com/gin-contrib/sse v0.1.0 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/go-playground/validator/v10 v10.14.0 // indirect 27 | github.com/goccy/go-json v0.10.2 // indirect 28 | github.com/golang/protobuf v1.5.3 // indirect 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 32 | github.com/leodido/go-urn v1.2.4 // indirect 33 | github.com/mattn/go-isatty v0.0.19 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 40 | github.com/ugorji/go/codec v1.2.11 // indirect 41 | golang.org/x/arch v0.3.0 // indirect 42 | golang.org/x/crypto v0.13.0 // indirect 43 | golang.org/x/mod v0.12.0 // indirect 44 | golang.org/x/net v0.15.0 // indirect 45 | golang.org/x/sys v0.12.0 // indirect 46 | golang.org/x/text v0.13.0 // indirect 47 | golang.org/x/tools v0.13.0 // indirect 48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 2 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 3 | github.com/ayonli/goext v0.4.0 h1:+9BgmgHmoyK941g2RMPFkSsdPu38w6Z2++rmXPt0jGs= 4 | github.com/ayonli/goext v0.4.0/go.mod h1:mlLQ4krsB+F7OGLrrAD6njBKaTKeu0AJey5YgaLI0o0= 5 | github.com/ayonli/goext v0.4.1 h1:7btY6HuFlU1Q/4A9FyDcpxehr4jjujSlmo6RiggqYvc= 6 | github.com/ayonli/goext v0.4.1/go.mod h1:mlLQ4krsB+F7OGLrrAD6njBKaTKeu0AJey5YgaLI0o0= 7 | github.com/ayonli/goext v0.4.2 h1:/OWg9YQ5f9R2DCzYO67JVWtsKB0eJgtYuW3m4hJQf7I= 8 | github.com/ayonli/goext v0.4.2/go.mod h1:mlLQ4krsB+F7OGLrrAD6njBKaTKeu0AJey5YgaLI0o0= 9 | github.com/ayonli/goext v0.4.3 h1:FsQgp4+y8vU7pjfMHA/qP92UxBJjMV6SvFQAgGZ+n0Y= 10 | github.com/ayonli/goext v0.4.3/go.mod h1:mlLQ4krsB+F7OGLrrAD6njBKaTKeu0AJey5YgaLI0o0= 11 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 12 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 13 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 14 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 15 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 22 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 26 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 29 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 30 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 31 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 32 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 33 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 34 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 35 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 36 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 37 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 38 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 39 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 42 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 43 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 45 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 46 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 47 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 48 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 49 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 50 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 51 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 52 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 53 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 54 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 56 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 57 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 61 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 62 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 63 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4= 67 | github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g= 68 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 69 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 70 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 71 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 72 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 75 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 76 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 77 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 81 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 82 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 83 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 84 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 85 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 86 | github.com/struCoder/pidusage v0.2.1 h1:dFiEgUDkubeIj0XA1NpQ6+8LQmKrLi7NiIQl86E6BoY= 87 | github.com/struCoder/pidusage v0.2.1/go.mod h1:bewtP2KUA1TBUyza5+/PCpSQ6sc/H6jJbIKAzqW86BA= 88 | github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= 89 | github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= 90 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 91 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 92 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 93 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 94 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 95 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 96 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 97 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 98 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 99 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 100 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 101 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 102 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 103 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 104 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 105 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 108 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 110 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 111 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 112 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 113 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 115 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 116 | google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= 117 | google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= 118 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 119 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 120 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 121 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 122 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 125 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 126 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 127 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 128 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import ngrpc from "./app"; 2 | export * from "./app"; 3 | export { service } from "./util"; 4 | export default ngrpc; 5 | -------------------------------------------------------------------------------- /ngrpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./ngrpc.schema.json", 3 | "protoPaths": [ 4 | "proto" 5 | ], 6 | "protoOptions": { 7 | "defaults": true 8 | }, 9 | "apps": [ 10 | { 11 | "name": "example-server", 12 | "url": "grpc://localhost:4000", 13 | "serve": true, 14 | "services": [ 15 | "services.ExampleService" 16 | ], 17 | "entry": "entry/main.ts", 18 | "stdout": "out.log" 19 | }, 20 | { 21 | "name": "user-server", 22 | "url": "grpcs://localhost:4001", 23 | "serve": true, 24 | "services": [ 25 | "services.UserService" 26 | ], 27 | "entry": "entry/main.go", 28 | "stdout": "out.log", 29 | "cert": "certs/cert.pem", 30 | "key": "certs/cert.key", 31 | "ca": "certs/ca.pem" 32 | }, 33 | { 34 | "name": "post-server", 35 | "url": "grpcs://localhost:4002", 36 | "serve": true, 37 | "services": [ 38 | "services.PostService" 39 | ], 40 | "entry": "entry/main.ts", 41 | "stdout": "out.log", 42 | "cert": "certs/cert.pem", 43 | "key": "certs/cert.key", 44 | "ca": "certs/ca.pem" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /ngrpc.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "https://github.com/ayonli/ngrpc/blob/main/ngrpc.schema.json", 4 | "title": "NgRPC Config", 5 | "description": "The configuration for NgRPC", 6 | "type": "object", 7 | "properties": { 8 | "namespace": { 9 | "type": "string", 10 | "description": "The namespace of the services files, default `services`." 11 | }, 12 | "tsconfig": { 13 | "type": "string", 14 | "description": "Custom tsconfig.json file for compiling TypeScript files." 15 | }, 16 | "importRoot": { 17 | "type": "string", 18 | "description": "Where to begin searching for TypeScript / JavaScript files, the default is `.`." 19 | }, 20 | "protoPaths": { 21 | "type": "array", 22 | "description": "The directories that contains `.proto` files.", 23 | "items": { 24 | "type": "string" 25 | } 26 | }, 27 | "protoOptions": { 28 | "type": "object", 29 | "description": "These options are used when loading the `.proto` files.", 30 | "properties": { 31 | "keepCase": { 32 | "type": "boolean", 33 | "description": "Preserve field names. The default is to change them to camel case." 34 | }, 35 | "longs": { 36 | "type": "string", 37 | "description": "The type to use to represent long values. Defaults to a Long object type.", 38 | "enum": [ 39 | "String", 40 | "Number" 41 | ] 42 | }, 43 | "enums": { 44 | "type": "string", 45 | "description": "The type to use to represent enum values. Defaults to the numeric value.", 46 | "enum": [ 47 | "String" 48 | ] 49 | }, 50 | "bytes": { 51 | "type": "string", 52 | "description": "The type to use to represent bytes values. Defaults to Buffer.", 53 | "enum": [ 54 | "Array", 55 | "String" 56 | ] 57 | }, 58 | "defaults": { 59 | "type": "boolean", 60 | "description": "Set default values on output objects. Defaults to false." 61 | }, 62 | "arrays": { 63 | "type": "boolean", 64 | "description": "Set empty arrays for missing array values even if defaults is false Defaults to false." 65 | }, 66 | "objects": { 67 | "type": "boolean", 68 | "description": "Set empty objects for missing object values even if defaults is false Defaults to false." 69 | }, 70 | "oneofs": { 71 | "type": "boolean", 72 | "description": "Set virtual oneof properties to the present field's name. Defaults to false." 73 | }, 74 | "json": { 75 | "type": "boolean", 76 | "description": "Represent Infinity and NaN as strings in float fields, and automatically decode google.protobuf.Any values. Defaults to false" 77 | }, 78 | "includeDirs": { 79 | "type": "array", 80 | "description": "A list of search paths for imported .proto files.", 81 | "items": { 82 | "type": "string" 83 | } 84 | } 85 | } 86 | }, 87 | "apps": { 88 | "type": "array", 89 | "description": "This property configures the apps that this project serves and connects.", 90 | "items": { 91 | "type": "object", 92 | "properties": { 93 | "name": { 94 | "type": "string", 95 | "description": "The name of the app." 96 | }, 97 | "url": { 98 | "type": "string", 99 | "description": "The URL of the gRPC server, supported schemes are `grpc:`, `grpcs:`, `http:`, `https:` or `xds:`.", 100 | "pattern": "^(http|https|grpc|grpcs|xds)://*" 101 | }, 102 | "serve": { 103 | "type": "boolean", 104 | "description": "If this app is served by the NgRPC app server.", 105 | "default": false 106 | }, 107 | "services": { 108 | "description": "The services served by this app.", 109 | "type": "array", 110 | "items": { 111 | "type": "string" 112 | } 113 | }, 114 | "cert": { 115 | "type": "string", 116 | "description": "The certificate filename when using TLS/SSL." 117 | }, 118 | "key": { 119 | "type": "string", 120 | "description": "The private key filename when using TLS/SSL." 121 | }, 122 | "ca": { 123 | "type": "string", 124 | "description": "The CA filename used to verify the other peer's certificates, when omitted, the system's root CAs will be used." 125 | }, 126 | "connectTimeout": { 127 | "type": "integer", 128 | "description": "Connection timeout in milliseconds, the default value is `5_000` ms." 129 | }, 130 | "options": { 131 | "type": "object", 132 | "description": "Channel options, see https://www.npmjs.com/package/@grpc/grpc-js for more details." 133 | }, 134 | "stdout": { 135 | "type": "string", 136 | "description": "Log file used for stdout." 137 | }, 138 | "stderr": { 139 | "type": "string", 140 | "description": "Log file used for stderr." 141 | }, 142 | "entry": { 143 | "type": "string", 144 | "description": "The entry file used to spawn this app." 145 | }, 146 | "env": { 147 | "type": "object", 148 | "description": "Additional environment variables passed to the `entry` file." 149 | } 150 | }, 151 | "required": [ 152 | "name", 153 | "url", 154 | "services" 155 | ], 156 | "dependencies": { 157 | "options": [ 158 | "serve" 159 | ], 160 | "stdout": [ 161 | "serve" 162 | ], 163 | "stderr": [ 164 | "serve", 165 | "stdout" 166 | ], 167 | "args": [ 168 | "serve" 169 | ], 170 | "env": [ 171 | "serve" 172 | ] 173 | } 174 | } 175 | } 176 | }, 177 | "required": [ 178 | "protoPaths", 179 | "apps" 180 | ] 181 | } 182 | -------------------------------------------------------------------------------- /pack.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require("child_process"); 2 | const path = require("path"); 3 | const cfg = require("./package.json"); 4 | const pkg = "github.com/ayonli/ngrpc/cli/ngrpc"; 5 | 6 | /** @typedef {"linux" | "darwin" | "windows"} OS */ 7 | /** @typedef {"amd64" | "arm64"} Arch */ 8 | 9 | /** 10 | * @type {{ os: OS, arch: Arch[] }[]} targets 11 | */ 12 | const targets = [ 13 | { os: "linux", arch: ["amd64", "arm64"] }, 14 | { os: "darwin", arch: ["amd64", "arm64"] }, 15 | { os: "windows", arch: ["amd64", "arm64"] } 16 | ]; 17 | 18 | for (const { os, arch } of targets) { 19 | console.log("packing for", os, "..."); 20 | 21 | for (const _arch of arch) { 22 | const wd = path.join("prebuild", os, _arch); 23 | const exeName = os === "windows" ? "ngrpc.exe" : "ngrpc"; 24 | const outPath = path.join(wd, exeName); 25 | spawnSync("go", [ 26 | "build", 27 | "-o", 28 | outPath, 29 | `-ldflags`, 30 | `-X '${pkg}/cmd.version=v${cfg.version}'`, 31 | pkg 32 | ], { 33 | stdio: "inherit", 34 | env: { 35 | ...process.env, 36 | GOOS: os, 37 | GOARCH: _arch, 38 | } 39 | }); 40 | spawnSync("tar", [ 41 | "-czf", 42 | path.join("prebuild", `ngrpc-${os}-${_arch}.tgz`), 43 | "-C", 44 | wd, 45 | exeName 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ayonli/ngrpc", 3 | "version": "0.2.6", 4 | "description": "Make it easy to create clean and elegant gRPC based microservices.", 5 | "main": "dist/index.js", 6 | "types": "index.ts", 7 | "bin": { 8 | "ngrpc": "dist/cli/index.js" 9 | }, 10 | "scripts": { 11 | "prepublishOnly": "tsc", 12 | "test": "mocha" 13 | }, 14 | "engines": { 15 | "node": ">=14" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ayonli/ngrpc.git" 20 | }, 21 | "keywords": [ 22 | "grpc" 23 | ], 24 | "author": "A-yon Lee ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ayonli/ngrpc/issues" 28 | }, 29 | "homepage": "https://github.com/ayonli/ngrpc#readme", 30 | "dependencies": { 31 | "@ayonli/grpc-async": "^0.1.2", 32 | "@ayonli/jsext": "^0.9.56", 33 | "@grpc/grpc-js": "^1.11.1", 34 | "@grpc/proto-loader": "^0.7.13", 35 | "@types/lodash": "^4.14.198", 36 | "@types/string-hash": "^1.1.1", 37 | "follow-redirects": "^1.15.2", 38 | "js-magic": "^1.4.2", 39 | "lodash": "^4.17.21", 40 | "require-chain": "^2.1.0", 41 | "string-hash": "^1.1.3", 42 | "tar": "^6.2.0" 43 | }, 44 | "devDependencies": { 45 | "@ayonli/ngrpc": "file:../ngrpc", 46 | "@types/express": "^4.17.17", 47 | "@types/follow-redirects": "^1.14.1", 48 | "@types/humanize-duration": "^3.27.1", 49 | "@types/mocha": "^10.0.1", 50 | "@types/node": "^20.5.9", 51 | "@types/tar": "^6.1.5", 52 | "express": "^4.18.2", 53 | "mocha": "^10.2.0", 54 | "source-map-support": "^0.5.21", 55 | "ts-node": "^10.9.1", 56 | "tslib": "^2.6.2", 57 | "typescript": "^5.2.2" 58 | }, 59 | "mocha": { 60 | "require": [ 61 | "ts-node/register" 62 | ], 63 | "spec": [ 64 | "*.test.ts", 65 | "**/*.test.ts" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pm/guest.go: -------------------------------------------------------------------------------- 1 | package pm 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | "time" 15 | 16 | "github.com/ayonli/goext/slicex" 17 | "github.com/ayonli/goext/stringx" 18 | "github.com/ayonli/ngrpc/config" 19 | "github.com/ayonli/ngrpc/pm/socket" 20 | "github.com/ayonli/ngrpc/util" 21 | ) 22 | 23 | type ControlMessage struct { 24 | Cmd string `json:"cmd"` 25 | App string `json:"app"` 26 | MsgId string `json:"msgId"` 27 | Text string `json:"text"` 28 | Guests []clientRecord `json:"guests"` 29 | Error string `json:"error"` 30 | 31 | // `Pid` shall be provided when `Cmd` is `handshake`. 32 | Pid int `json:"pid"` 33 | 34 | // `conn.Close()` will destroy the connection before the final message is flushed, causing the 35 | // other peer losing the connection and the message, and no EOF will be received. To guarantee 36 | // the final message is sent, we need a signal (`FIN`) to indicate whether this is the final 37 | // message, and close the connection on the receiver side. 38 | Fin bool `json:"fin"` 39 | } 40 | 41 | func EncodeMessage(msg ControlMessage) []byte { 42 | buf, _ := json.Marshal(msg) 43 | buf = append(buf, byte('\n')) 44 | 45 | return buf 46 | } 47 | 48 | func DecodeMessage(packet *[]byte, bufRead []byte, eof bool) []ControlMessage { 49 | *packet = append(*packet, bufRead...) 50 | chunks := slicex.Split(*packet, byte('\n')) 51 | 52 | if eof { 53 | // Empty the packet when reaching EOF. 54 | *packet = []byte{} 55 | // Returns all non-empty chunks, normally the last chunk is empty. 56 | chunks = slicex.Filter(chunks, func(chunk []byte, _ int) bool { 57 | return len(chunk) > 0 58 | }) 59 | } else if len(chunks) > 1 { 60 | // The last chunk is unfinished, we store it in the packet for more data. 61 | *packet = chunks[len(chunks)-1] 62 | // All chunks (except the last one) will be processed. 63 | chunks = chunks[:len(chunks)-1] 64 | } else { // len(chunk) == 1 65 | // We use `\n` to delimit message packets, each packet ends with a `\n`, when len(chunks) 66 | // is 1, it means that the delimiter haven't been received and there is more buffers needs 67 | // to be received, no available chunks for consuming yet. 68 | return nil 69 | } 70 | 71 | messages := []ControlMessage{} 72 | 73 | for _, chunk := range chunks { 74 | var msg ControlMessage 75 | 76 | if err := json.Unmarshal(chunk, &msg); err != nil { 77 | continue 78 | } else { 79 | messages = append(messages, msg) 80 | } 81 | } 82 | 83 | return messages 84 | } 85 | 86 | func GetSocketPath() (sockFile string, sockPath string) { 87 | confFile := util.AbsPath("ngrpc.json", false) 88 | ext := filepath.Ext(confFile) 89 | sockFile = stringx.Slice(confFile, 0, -len(ext)) + ".sock" 90 | sockPath = util.AbsPath(sockFile, true) 91 | 92 | return sockFile, sockPath 93 | } 94 | 95 | func IsHostOnline() bool { 96 | sockFile, sockPath := GetSocketPath() 97 | 98 | if runtime.GOOS != "windows" && !util.Exists(sockFile) { 99 | return false 100 | } 101 | 102 | conn, err := socket.DialTimeout(sockPath, time.Second) 103 | 104 | if err != nil { 105 | os.Remove(sockFile) // The socket file is left by a unclean shutdown, remove it. 106 | return false 107 | } else { 108 | conn.Close() 109 | return true 110 | } 111 | } 112 | 113 | type Guest struct { 114 | AppName string 115 | AppUrl string 116 | conn net.Conn 117 | // 0: disconnected; 1: connected; 2: closed 118 | state int 119 | handleStopCommand func(msgId string) 120 | replyChan chan ControlMessage 121 | cancelSignal chan bool 122 | } 123 | 124 | func NewGuest(app config.App, onStopCommand func(msgId string)) *Guest { 125 | guest := &Guest{ 126 | AppName: app.Name, 127 | AppUrl: app.Url, 128 | handleStopCommand: onStopCommand, 129 | } 130 | 131 | return guest 132 | } 133 | 134 | func (self *Guest) Join() { 135 | err := self.connect() 136 | 137 | if err != nil { // auto-reconnect in the background 138 | go self.reconnect() 139 | } 140 | } 141 | 142 | func (self *Guest) connect() error { 143 | sockFile, sockPath := GetSocketPath() 144 | 145 | if !IsHostOnline() { 146 | return errors.New("host server is not running") 147 | } 148 | 149 | conn, err := socket.DialTimeout(sockPath, time.Second) 150 | 151 | if err != nil { 152 | // The socket file is left because a previous unclean shutdown, remove it so the filename 153 | // can be reused. 154 | os.Remove(sockFile) 155 | return err 156 | } 157 | 158 | msg := ControlMessage{ 159 | Cmd: "handshake", 160 | App: self.AppName, 161 | Pid: os.Getpid(), 162 | } 163 | 164 | _, err = conn.Write(EncodeMessage(msg)) 165 | 166 | if err != nil { 167 | conn.Close() 168 | return err 169 | } 170 | 171 | self.conn = conn 172 | handshake := make(chan bool) 173 | 174 | go func() { 175 | packet := []byte{} 176 | buf := make([]byte, 256) 177 | 178 | for { 179 | if n, err := conn.Read(buf); err != nil { 180 | if errors.Is(err, io.EOF) { 181 | self.processHostMessage(handshake, &packet, buf[:n], true) 182 | self.handleHostDisconnection() 183 | break 184 | } else if errors.Is(err, net.ErrClosed) || 185 | strings.Contains(err.Error(), "closed") { // go-winio error 186 | self.handleHostDisconnection() 187 | break 188 | } else { 189 | log.Println(err) 190 | } 191 | } else { 192 | self.processHostMessage(handshake, &packet, buf[:n], false) 193 | } 194 | } 195 | }() 196 | 197 | <-handshake 198 | 199 | if self.AppName != "" && self.AppName != ":cli" { 200 | log.Printf("app [%s] has joined the group", self.AppName) 201 | } 202 | 203 | return nil 204 | } 205 | 206 | func (self *Guest) Leave(reason string, replyId string) bool { 207 | if self.conn != nil { 208 | if replyId != "" { 209 | // If `replyId` is provided, that means the stop event is issued by a guest app, for 210 | // example, the CLI tool, in this case, we need to send feedback to acknowledge the 211 | // sender that the process has finished. 212 | // 213 | // Apparently there is some compatibility issues in the Golang's go-winio package, 214 | // if we sent the messages one by one continuously, go-winio cannot receive them 215 | // well. So we send them in one packet, allowing the host server to separate them 216 | // when received as a whole. 217 | self.Send(ControlMessage{Cmd: "goodbye", App: self.AppName}, ControlMessage{ 218 | Cmd: "reply", 219 | App: self.AppName, 220 | MsgId: replyId, 221 | Text: reason, 222 | Fin: true, 223 | }) 224 | } else { 225 | self.Send(ControlMessage{Cmd: "goodbye", App: self.AppName, Fin: true}) 226 | } 227 | } else if self.cancelSignal != nil { 228 | self.cancelSignal <- true 229 | } 230 | 231 | ok := self.state == 1 232 | self.state = 2 233 | return ok 234 | } 235 | 236 | func (self *Guest) Send(msg ...ControlMessage) error { 237 | packet := slicex.Flat(slicex.Map(msg, func(chunk ControlMessage, _ int) []byte { 238 | return EncodeMessage(chunk) 239 | })) 240 | _, err := self.conn.Write(packet) 241 | return err 242 | } 243 | 244 | func (self *Guest) reconnect() { 245 | self.cancelSignal = make(chan bool) 246 | loop: 247 | for self.state == 0 { 248 | select { 249 | case <-time.After(time.Second): 250 | if self.state == 2 { 251 | break loop 252 | } else { 253 | err := self.connect() 254 | 255 | if err == nil { 256 | break loop 257 | } 258 | } 259 | case <-self.cancelSignal: 260 | close(self.cancelSignal) 261 | break loop 262 | } 263 | } 264 | } 265 | 266 | func (self *Guest) handleHostDisconnection() { 267 | if self.state == 0 { 268 | return 269 | } else if self.state != 2 { 270 | self.state = 0 271 | self.reconnect() 272 | } 273 | } 274 | 275 | func (self *Guest) processHostMessage( 276 | handshake chan bool, 277 | packet *[]byte, 278 | bufRead []byte, 279 | eof bool, 280 | ) { 281 | for _, msg := range DecodeMessage(packet, bufRead, eof) { 282 | self.handleMessage(handshake, msg) 283 | } 284 | } 285 | 286 | func (self *Guest) handleMessage(handshake chan bool, msg ControlMessage) { 287 | if msg.Cmd == "handshake" { 288 | self.state = 1 289 | handshake <- true 290 | close(handshake) 291 | } else if msg.Cmd == "goodbye" { 292 | self.conn.Close() 293 | 294 | if self.replyChan != nil { 295 | self.replyChan <- msg 296 | } 297 | } else if msg.Cmd == "stop" { 298 | self.handleStopCommand(msg.MsgId) 299 | } else if msg.Cmd == "reload" { 300 | self.Send(ControlMessage{ 301 | Cmd: "reply", 302 | MsgId: msg.MsgId, 303 | Text: fmt.Sprintf("app [%v] does not support hot-reloading", self.AppName), 304 | }) 305 | } else if msg.Cmd == "reply" || msg.Cmd == "online" { 306 | if self.replyChan != nil { 307 | self.replyChan <- msg 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /pm/guest.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert"; 2 | import * as path from "node:path"; 3 | import _try from "@ayonli/jsext/try"; 4 | import { writeFile, exists } from "@ayonli/jsext/fs"; 5 | import { test } from "mocha"; 6 | import { App } from "../app"; 7 | import { Guest, ControlMessage, encodeMessage, decodeMessage, getSocketPath } from "./guest"; 8 | 9 | function newMsg(msg: ControlMessage): ControlMessage { 10 | msg.app ??= ""; 11 | msg.error ??= ""; 12 | msg.fin ??= false; 13 | msg.msgId ??= ""; 14 | msg.pid ??= 0; 15 | msg.guests ??= []; 16 | msg.text ??= ""; 17 | 18 | return { ...msg }; 19 | } 20 | 21 | test("encodeMessage", () => { 22 | const msg = encodeMessage(newMsg({ cmd: "stop", app: "example-server", msgId: "abc" })); 23 | assert.strictEqual(msg[msg.length - 1], "\n"); 24 | }); 25 | 26 | test("decodeMessage", () => { 27 | const msg: ControlMessage = newMsg({ cmd: "stop", app: "example-server", msgId: "abc" }); 28 | const data = encodeMessage(msg); 29 | let packet = ""; 30 | let buf = data.slice(0, 256); 31 | 32 | const { packet: _packet, messages } = decodeMessage(packet, buf, false); 33 | packet = _packet; 34 | 35 | assert.strictEqual(messages.length, 1); 36 | assert.deepStrictEqual(messages[0], msg); 37 | assert.strictEqual(packet, ""); 38 | }); 39 | 40 | test("decodeMessage overflow", () => { 41 | const msg: ControlMessage = newMsg({ cmd: "stop", app: "example-server", msgId: "abc" }); 42 | const data = encodeMessage(msg); 43 | let packet = ""; 44 | let buf = data.slice(0, 64); 45 | let offset = 0; 46 | 47 | let result = decodeMessage(packet, buf, false); 48 | let messages = result.messages; 49 | packet = result.packet; 50 | offset += 64; 51 | 52 | assert.strictEqual(messages.length, 0); 53 | assert.strictEqual(packet, buf); 54 | 55 | while (offset < data.length) { 56 | buf = data.slice(offset, offset + 64); 57 | const result = decodeMessage(packet, buf, false); 58 | messages = result.messages; 59 | packet = result.packet; 60 | offset += 64; 61 | } 62 | 63 | assert.strictEqual(messages.length, 1); 64 | assert.deepStrictEqual(messages[0], msg); 65 | assert.strictEqual(packet, ""); 66 | }); 67 | 68 | test("decodeMessage EOF", () => { 69 | const msg: ControlMessage = newMsg({ cmd: "stop", app: "example-server", msgId: "abc" }); 70 | const data = encodeMessage(msg).slice(0, -1); 71 | let packet = ""; 72 | let buf = data.slice(0, 256); 73 | 74 | let result = decodeMessage(packet, buf, true); 75 | let messages = result.messages; 76 | packet = result.packet; 77 | 78 | assert.strictEqual(messages.length, 1); 79 | assert.deepStrictEqual(messages[0], msg); 80 | assert.strictEqual(packet, ""); 81 | }); 82 | 83 | test("getSocketPath", () => { 84 | const cwd = process.cwd(); 85 | const { sockFile, sockPath } = getSocketPath(); 86 | 87 | assert.strictEqual(sockFile, path.join(cwd, "ngrpc.sock")); 88 | 89 | if (process.platform === "win32") { 90 | assert.strictEqual(sockPath, "\\\\.\\pipe\\" + path.join(cwd, "ngrpc.sock")); 91 | } else { 92 | assert.strictEqual(sockPath, path.join(cwd, "ngrpc.sock")); 93 | } 94 | }); 95 | 96 | test("new Guest", () => { 97 | const app: App = { 98 | name: "example-server", 99 | url: "grpc://localhost:4000", 100 | services: [], 101 | }; 102 | const handleStop = () => void 0; 103 | const handleReload = () => void 0; 104 | const guest = new Guest(app, { onStopCommand: handleStop, onReloadCommand: handleReload }); 105 | 106 | assert.strictEqual(guest.appName, app.name); 107 | assert.strictEqual(guest.appUrl, app.url); 108 | assert.strictEqual(guest["handleStopCommand"], handleStop); 109 | assert.strictEqual(guest["handleReloadCommand"], handleReload); 110 | }); 111 | 112 | test("Guest join redundant socket file", async () => { 113 | const { sockFile } = getSocketPath(); 114 | await writeFile(sockFile, new Uint8Array([])); 115 | 116 | assert.ok(await exists(sockFile)); 117 | 118 | const guest = new Guest({ 119 | name: "example-server", 120 | url: "grpc://localhost:4000", 121 | services: [], 122 | }, { 123 | onStopCommand: () => void 0, 124 | onReloadCommand: () => void 0, 125 | }); 126 | const [err] = await _try(() => guest["connect"]()); 127 | 128 | assert.ok(!!err); 129 | assert.ok(!(await exists(sockFile))); 130 | }); 131 | -------------------------------------------------------------------------------- /pm/guest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import * as net from "node:net"; 3 | import { exists, remove } from "@ayonli/jsext/fs"; 4 | import { absPath, timed } from "../util"; 5 | import type { App } from "../app"; 6 | 7 | export interface ControlMessage { 8 | cmd: "handshake" | "goodbye" | "reply" | "stop" | "reload"; 9 | app?: string; 10 | msgId?: string; 11 | text?: string; 12 | guests?: { 13 | app: string; 14 | pid: number; 15 | startTime: number; 16 | }[]; 17 | error?: string; 18 | 19 | // `pid` shall be provided when `cmd` is `handshake`. 20 | pid?: number; 21 | 22 | // Indicates that this is the last message, after set true, the socket connection will be closed 23 | // by the receiver peer. 24 | fin?: boolean; 25 | } 26 | 27 | export function encodeMessage(msg: ControlMessage): string { 28 | return JSON.stringify(msg) + "\n"; 29 | } 30 | 31 | export function decodeMessage(packet: string, buf: string, eof = false): { 32 | packet: string; 33 | messages: ControlMessage[]; 34 | } { 35 | packet += buf; 36 | let chunks = packet.split("\n"); 37 | 38 | if (eof) { 39 | // Empty the packet when reaching EOF. 40 | packet = ""; 41 | // Returns all non-empty chunks, normally the last chunk is empty. 42 | chunks = chunks.filter(chunk => chunk.length > 0); 43 | } else if (chunks.length > 1) { 44 | // The last chunk is unfinished, we store it in the packet for more data. 45 | packet = chunks[chunks.length - 1] as string; 46 | // All chunks (except the last one) will be processed. 47 | chunks = chunks.slice(0, -1); 48 | } else { // chunks.length === 1 49 | // We use `\n` to delimit message packets, each packet ends with a `\n`, when len(chunks) 50 | // is 1, it means that the delimiter haven't been received and there is more buffers needs 51 | // to be received, no available chunks for consuming yet. 52 | return { packet, messages: [] }; 53 | } 54 | 55 | const messages: ControlMessage[] = []; 56 | 57 | for (const chunk of chunks) { 58 | try { 59 | const msg = JSON.parse(chunk); 60 | messages.push(msg); 61 | } catch { } 62 | } 63 | 64 | return { packet, messages }; 65 | } 66 | 67 | export function getSocketPath() { 68 | const confFile = absPath("ngrpc.json"); 69 | const ext = path.extname(confFile); 70 | const sockFile = confFile.slice(0, -ext.length) + ".sock"; 71 | const sockPath = absPath(sockFile, true); 72 | 73 | return { sockFile, sockPath }; 74 | } 75 | 76 | export class Guest { 77 | appName: string; 78 | appUrl: string; 79 | /** 0: disconnected; 1: connected; 2: closed */ 80 | private state = 0; 81 | private conn: net.Socket | undefined; 82 | private reconnector: NodeJS.Timeout | null = null; 83 | private handleStopCommand: (msgId: string | undefined) => void; 84 | private handleReloadCommand: (msgId: string | undefined) => void; 85 | 86 | constructor(app: App, options: { 87 | onStopCommand: (msgId: string | undefined) => void; 88 | onReloadCommand: (msgId: string | undefined) => void; 89 | }) { 90 | this.appName = app.name; 91 | this.appUrl = app.url; 92 | this.handleStopCommand = options?.onStopCommand; 93 | this.handleReloadCommand = options?.onReloadCommand; 94 | } 95 | 96 | async join() { 97 | try { 98 | await this.connect(); 99 | } catch { 100 | this.reconnect(); // auto-reconnect in the background 101 | } 102 | } 103 | 104 | private async connect(): Promise { 105 | const { sockFile, sockPath } = getSocketPath(); 106 | 107 | if (process.platform !== "win32" && !(await exists(sockFile))) { 108 | throw new Error("host server is not running"); 109 | } 110 | 111 | await new Promise(async (handshake, reject) => { 112 | const connectFailureHandler = async (err: Error) => { 113 | try { await remove(sockFile); } catch { } 114 | reject(err); 115 | }; 116 | 117 | const conn = net.createConnection(sockPath, () => { 118 | this.conn = conn; 119 | this.send({ cmd: "handshake", app: this.appName, pid: process.pid }); 120 | conn.off("error", connectFailureHandler); 121 | 122 | (async () => { 123 | let packet = ""; 124 | 125 | try { 126 | for await (const buf of conn) { 127 | packet = this.processHostMessage( 128 | handshake, 129 | packet, 130 | (buf as Buffer).toString()); 131 | } 132 | } catch (err) { 133 | if (this.conn?.destroyed || this.conn?.closed) { 134 | this.handleHostDisconnection(); 135 | } else { 136 | console.error(timed`${err}`); 137 | } 138 | } 139 | })(); 140 | }); 141 | conn.once("error", connectFailureHandler); 142 | }); 143 | 144 | if (this.appName) { 145 | console.log(timed`app [${this.appName}] has joined the group`); 146 | } 147 | } 148 | 149 | async leave(reason: string, replyId = ""): Promise { 150 | if (this.state !== 0) { 151 | if (replyId) { 152 | // If `replyId` is provided, that means the stop event is issued by a guest app, for 153 | // example, the CLI tool, in this case, we need to send feedback to acknowledge the 154 | // sender that the process has finished. 155 | // 156 | // Apparently there is some compatibility issues in the Golang's go-winio package, 157 | // if we sent the messages one by one continuously, go-winio cannot receive them 158 | // well. So we send them in one packet, allowing the host server to separate them 159 | // when received as a whole. 160 | this.send({ cmd: "goodbye", app: this.appName }, { 161 | cmd: "reply", 162 | app: this.appName, 163 | msgId: replyId, 164 | text: reason, 165 | fin: true, 166 | }); 167 | } else { 168 | this.send({ cmd: "goodbye", app: this.appName, fin: true }); 169 | } 170 | } else if (this.reconnector) { 171 | clearInterval(this.reconnector); 172 | this.reconnector = null; 173 | } 174 | 175 | const ok = this.state == 1; 176 | this.state = 2; 177 | return ok; 178 | } 179 | 180 | async send(...msg: ControlMessage[]) { 181 | this.conn?.write(msg.map(m => encodeMessage(m)).join("")); 182 | } 183 | 184 | private async reconnect() { 185 | this.reconnector = setInterval(async () => { 186 | if (this.state === 2) { 187 | this.reconnector && clearInterval(this.reconnector); 188 | this.reconnector = null; 189 | } else { 190 | try { 191 | await this.connect(); 192 | 193 | if (this.state !== 0) { 194 | this.reconnector && clearInterval(this.reconnector); 195 | this.reconnector = null; 196 | } 197 | } catch { } 198 | } 199 | }, 1_000); 200 | } 201 | 202 | private handleHostDisconnection() { 203 | if (this.state === 0) { 204 | return; 205 | } else if (this.state !== 2) { 206 | this.state = 0; 207 | this.reconnect(); 208 | } 209 | } 210 | 211 | private processHostMessage(handshake: () => void, packet: string, buf: string) { 212 | const res = decodeMessage(packet, buf, false); 213 | 214 | for (const msg of res.messages) { 215 | this.handleMessage(handshake, msg); 216 | } 217 | 218 | return packet; 219 | } 220 | 221 | private handleMessage(handshake: () => void, msg: ControlMessage) { 222 | if (msg.cmd === "handshake") { 223 | this.state = 1; 224 | handshake(); 225 | } else if (msg.cmd === "goodbye") { 226 | this.conn?.destroy(); 227 | } else if (msg.cmd === "stop") { 228 | this.handleStopCommand(msg.msgId); 229 | } else if (msg.cmd === "reload") { 230 | this.handleReloadCommand(msg.msgId); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /pm/guest_test.go: -------------------------------------------------------------------------------- 1 | package pm 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ayonli/goext" 11 | "github.com/ayonli/goext/slicex" 12 | "github.com/ayonli/ngrpc/config" 13 | "github.com/ayonli/ngrpc/util" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestEncodeMessage(t *testing.T) { 18 | msg := EncodeMessage(ControlMessage{Cmd: "stop", App: "example-server", MsgId: "abc"}) 19 | assert.Equal(t, uint8(10), msg[len(msg)-1]) 20 | } 21 | 22 | func TestDecodeMessage(t *testing.T) { 23 | msg := ControlMessage{Cmd: "stop", App: "example-server", MsgId: "abc"} 24 | data := EncodeMessage(msg) 25 | packet := []byte{} 26 | buf := make([]byte, 256) 27 | n := copy(buf, data) 28 | 29 | messages := DecodeMessage(&packet, buf[:n], false) 30 | 31 | assert.Equal(t, 1, len(messages)) 32 | assert.Equal(t, msg, messages[0]) 33 | assert.Equal(t, []byte{}, packet) 34 | } 35 | 36 | func TestDecodeMessageOverflow(t *testing.T) { 37 | msg := ControlMessage{Cmd: "stop", App: "example-server", MsgId: "abc"} 38 | data := EncodeMessage(msg) 39 | packet := []byte{} 40 | buf := make([]byte, 64) 41 | n := copy(buf, data) 42 | offset := 0 43 | 44 | messages := DecodeMessage(&packet, buf[:n], false) 45 | offset += 64 46 | 47 | assert.Equal(t, 0, len(messages)) 48 | assert.Equal(t, buf, packet) 49 | 50 | for offset < len(data) { 51 | n = copy(buf, data[offset:]) 52 | messages = DecodeMessage(&packet, buf[:n], false) 53 | offset += 64 54 | } 55 | 56 | assert.Equal(t, 1, len(messages)) 57 | assert.Equal(t, msg, messages[0]) 58 | assert.Equal(t, []byte{}, packet) 59 | } 60 | 61 | func TestDecodeMessageEof(t *testing.T) { 62 | msg := ControlMessage{Cmd: "stop", App: "example-server", MsgId: "abc"} 63 | data := slicex.Slice(EncodeMessage(msg), 0, -1) 64 | packet := []byte{} 65 | buf := make([]byte, 256) 66 | n := copy(buf, data) 67 | 68 | messages := DecodeMessage(&packet, buf[:n], true) 69 | 70 | assert.Equal(t, 1, len(messages)) 71 | assert.Equal(t, msg, messages[0]) 72 | assert.Equal(t, []byte{}, packet) 73 | } 74 | 75 | func TestGetSocketPath(t *testing.T) { 76 | cwd, _ := os.Getwd() 77 | sockFile, sockPath := GetSocketPath() 78 | 79 | assert.Equal(t, filepath.Join(cwd, "ngrpc.sock"), sockFile) 80 | 81 | if runtime.GOOS == "windows" { 82 | assert.Equal(t, "\\\\.\\pipe\\"+filepath.Join(cwd, "ngrpc.sock"), sockPath) 83 | } else { 84 | assert.Equal(t, filepath.Join(cwd, "ngrpc.sock"), sockPath) 85 | } 86 | } 87 | 88 | func TestIsHostOnline(t *testing.T) { 89 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 90 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 91 | defer os.Remove("ngrpc.json") 92 | defer os.Remove("tsconfig.json") 93 | 94 | assert.False(t, IsHostOnline()) 95 | 96 | conf := goext.Ok(config.LoadConfig()) 97 | host := NewHost(conf, false) 98 | goext.Ok(0, host.Start(false)) 99 | defer host.Stop() 100 | 101 | assert.True(t, IsHostOnline()) 102 | } 103 | 104 | func TestIsHostOnline_redundantSocketFile(t *testing.T) { 105 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 106 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 107 | defer os.Remove("ngrpc.json") 108 | defer os.Remove("tsconfig.json") 109 | 110 | sockFile, _ := GetSocketPath() 111 | os.WriteFile(sockFile, []byte{}, 0644) 112 | 113 | if runtime.GOOS != "windows" { 114 | assert.True(t, util.Exists(sockFile)) 115 | } 116 | 117 | assert.False(t, IsHostOnline()) 118 | 119 | if runtime.GOOS != "windows" { 120 | assert.False(t, util.Exists(sockFile)) 121 | } 122 | } 123 | 124 | func TestNewGuest(t *testing.T) { 125 | app := config.App{ 126 | Name: "example-server", 127 | Url: "grpc://localhost:4000", 128 | } 129 | handleStop := func(msgId string) {} 130 | guest := NewGuest(app, handleStop) 131 | 132 | assert.Equal(t, app.Name, guest.AppName) 133 | assert.Equal(t, app.Url, guest.AppUrl) 134 | assert.Equal(t, 0, guest.state) 135 | assert.NotNil(t, guest.handleStopCommand) 136 | } 137 | 138 | func TestGuest_JoinAndLeave(t *testing.T) { 139 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 140 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 141 | defer os.Remove("ngrpc.json") 142 | defer os.Remove("tsconfig.json") 143 | 144 | cfg := goext.Ok(config.LoadConfig()) 145 | host := NewHost(cfg, false) 146 | goext.Ok(0, host.Start(false)) 147 | defer host.Stop() 148 | 149 | c := make(chan string) 150 | guest := NewGuest(config.App{ 151 | Name: "example-server", 152 | Url: "grpc://localhost:4000", 153 | }, func(msgId string) { 154 | c <- msgId 155 | }) 156 | guest.Join() 157 | 158 | assert.Equal(t, 1, guest.state) 159 | assert.Equal(t, 1, len(host.clients)) 160 | 161 | guest.Leave("app [example-server] stopped", "") 162 | 163 | time.Sleep(time.Millisecond * 10) // wait a while for the host to close the connection 164 | assert.Equal(t, 2, guest.state) 165 | assert.Equal(t, 0, len(host.clients)) 166 | } 167 | 168 | func TestGuest_JoinRedundantSocketFile(t *testing.T) { 169 | sockFile, _ := GetSocketPath() 170 | os.WriteFile(sockFile, []byte{}, 0644) 171 | 172 | assert.True(t, util.Exists(sockFile)) 173 | 174 | guest := NewGuest(config.App{ 175 | Name: "example-server", 176 | Url: "grpc://localhost:4000", 177 | }, func(msgId string) {}) 178 | err := guest.connect() 179 | 180 | assert.NotNil(t, err) 181 | assert.False(t, util.Exists(sockFile)) 182 | } 183 | -------------------------------------------------------------------------------- /pm/host_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package pm 5 | 6 | import ( 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/ayonli/goext" 16 | "github.com/ayonli/goext/async" 17 | "github.com/ayonli/ngrpc/config" 18 | "github.com/ayonli/ngrpc/util" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestNewHost(t *testing.T) { 23 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 24 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 25 | defer os.Remove("ngrpc.json") 26 | defer os.Remove("tsconfig.json") 27 | 28 | config := goext.Ok(config.LoadConfig()) 29 | host := NewHost(config, false) 30 | 31 | assert.Equal(t, config.Apps, host.apps) 32 | assert.Equal(t, 0, host.state) 33 | assert.Equal(t, []clientRecord{}, host.clients) 34 | assert.Equal(t, 0, host.callbacks.Size()) 35 | } 36 | 37 | func TestHost_Start(t *testing.T) { 38 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 39 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 40 | defer os.Remove("ngrpc.json") 41 | defer os.Remove("tsconfig.json") 42 | 43 | config := goext.Ok(config.LoadConfig()) 44 | host := NewHost(config, false) 45 | err := host.Start(false) 46 | defer host.Stop() 47 | 48 | assert.Nil(t, err) 49 | assert.Equal(t, 1, host.state) 50 | assert.NotNil(t, host.server) 51 | assert.Equal(t, filepath.Join(goext.Ok(os.Getwd()), "ngrpc.sock"), host.sockFile) 52 | 53 | host2 := NewHost(config, false) 54 | err2 := host2.Start(false) 55 | 56 | if runtime.GOOS == "windows" { 57 | assert.Contains(t, err2.Error(), "Access is denied") 58 | } else { 59 | assert.Contains(t, err2.Error(), "address already in use") 60 | } 61 | 62 | assert.Equal(t, 0, host2.state) 63 | assert.Nil(t, host2.server) 64 | assert.Equal(t, "", host2.sockFile) 65 | } 66 | 67 | func TestHost_Stop(t *testing.T) { 68 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 69 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 70 | defer os.Remove("ngrpc.json") 71 | defer os.Remove("tsconfig.json") 72 | 73 | config := goext.Ok(config.LoadConfig()) 74 | host := NewHost(config, false) 75 | err := host.Start(false) 76 | 77 | assert.Nil(t, err) 78 | 79 | if runtime.GOOS != "windows" { 80 | assert.True(t, util.Exists(host.sockFile)) 81 | } 82 | 83 | host.Stop() 84 | 85 | assert.Equal(t, 0, host.state) 86 | 87 | if runtime.GOOS != "windows" { 88 | assert.False(t, util.Exists(host.sockFile)) 89 | } 90 | } 91 | 92 | func TestSendCommand_stop(t *testing.T) { 93 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 94 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 95 | defer os.Remove("ngrpc.json") 96 | defer os.Remove("tsconfig.json") 97 | 98 | conf := goext.Ok(config.LoadConfig()) 99 | host := NewHost(conf, false) 100 | goext.Ok(0, host.Start(false)) 101 | defer host.Stop() 102 | 103 | c := make(chan string) 104 | guest := NewGuest(config.App{ 105 | Name: "example-server", 106 | Url: "grpc://localhost:4000", 107 | }, func(msgId string) { 108 | c <- msgId 109 | }) 110 | guest.Join() 111 | 112 | assert.Equal(t, 1, guest.state) 113 | assert.Equal(t, 1, len(host.clients)) 114 | 115 | go func() { 116 | SendCommand("stop", "example-server") 117 | }() 118 | 119 | msgId := <-c 120 | guest.Leave("app [example-server] stopped", msgId) 121 | 122 | assert.NotEqual(t, "", msgId) 123 | assert.Equal(t, 2, guest.state) 124 | 125 | time.Sleep(time.Millisecond * 10) 126 | assert.Equal(t, 0, len(host.clients)) 127 | } 128 | 129 | func TestSendCommand_stopAll(t *testing.T) { 130 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 131 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 132 | defer os.Remove("ngrpc.json") 133 | defer os.Remove("tsconfig.json") 134 | 135 | conf := goext.Ok(config.LoadConfig()) 136 | host := NewHost(conf, false) 137 | goext.Ok(0, host.Start(false)) 138 | defer host.Stop() 139 | 140 | c := make(chan []string) 141 | 142 | msgIds := []string{} 143 | push := async.Queue(func(msgId string) (fin bool) { 144 | msgIds = append(msgIds, msgId) 145 | fin = len(msgIds) == 2 146 | 147 | if fin { 148 | c <- msgIds 149 | } 150 | return fin 151 | }) 152 | 153 | guest1 := NewGuest(config.App{ 154 | Name: "example-server", 155 | Url: "grpc://localhost:4000", 156 | }, push) 157 | guest2 := NewGuest(config.App{ 158 | Name: "user-server", 159 | Url: "grpc://localhost:4001", 160 | }, push) 161 | guest1.Join() 162 | guest2.Join() 163 | 164 | go func() { 165 | SendCommand("stop", "") 166 | }() 167 | 168 | <-c 169 | guest1.Leave("app [example-server] stopped", msgIds[0]) 170 | guest2.Leave("app [user-server] stopped", msgIds[1]) 171 | 172 | time.Sleep(time.Second) // after a second, all clients shall be closed, including the :cli 173 | assert.Equal(t, 0, len(host.clients)) 174 | } 175 | 176 | func TestSendCommand_list(t *testing.T) { 177 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 178 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 179 | defer os.Remove("ngrpc.json") 180 | defer os.Remove("tsconfig.json") 181 | 182 | conf := goext.Ok(config.LoadConfig()) 183 | host := NewHost(conf, false) 184 | goext.Ok(0, host.Start(false)) 185 | defer host.Stop() 186 | 187 | c := make(chan []string) 188 | 189 | msgIds := []string{} 190 | push := async.Queue(func(msgId string) (fin bool) { 191 | msgIds = append(msgIds, msgId) 192 | fin = len(msgIds) == 2 193 | 194 | if fin { 195 | c <- msgIds 196 | } 197 | return fin 198 | }) 199 | 200 | go func() { 201 | SendCommand("list", "") 202 | 203 | time.Sleep(time.Millisecond * 10) 204 | SendCommand("list", "") 205 | 206 | time.Sleep(time.Millisecond * 10) 207 | SendCommand("list", "") 208 | 209 | time.Sleep(time.Millisecond * 10) 210 | SendCommand("stop", "") 211 | }() 212 | 213 | time.Sleep(time.Millisecond * 10) 214 | guest1 := NewGuest(config.App{ 215 | Name: "example-server", 216 | Url: "grpc://localhost:4000", 217 | }, push) 218 | guest1.Join() 219 | 220 | time.Sleep(time.Millisecond * 10) 221 | guest2 := NewGuest(config.App{ 222 | Name: "user-server", 223 | Url: "grpc://localhost:4001", 224 | }, push) 225 | guest2.Join() 226 | 227 | <-c 228 | 229 | guest1.Leave("app [example-server] stopped", "") 230 | guest2.Leave("app [user-server] stopped", "") 231 | 232 | time.Sleep(time.Microsecond * 10) 233 | } 234 | 235 | func TestSendCommand_stopHost(t *testing.T) { 236 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 237 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 238 | defer os.Remove("ngrpc.json") 239 | defer os.Remove("tsconfig.json") 240 | 241 | conf := goext.Ok(config.LoadConfig()) 242 | host := NewHost(conf, false) 243 | goext.Ok(0, host.Start(false)) 244 | defer host.Stop() 245 | 246 | guest := NewGuest(config.App{ 247 | Name: "example-server", 248 | Url: "grpc://localhost:4000", 249 | }, func(msgId string) {}) 250 | guest.Join() 251 | 252 | assert.Equal(t, 1, guest.state) 253 | assert.Equal(t, 1, len(host.clients)) 254 | 255 | go func() { 256 | SendCommand("stop-host", "") 257 | }() 258 | 259 | time.Sleep(time.Second) // after a second, all clients shall be closed, including the :cli 260 | 261 | assert.Equal(t, 0, guest.state) 262 | assert.Equal(t, 0, host.state) 263 | assert.Equal(t, 0, len(host.clients)) 264 | } 265 | 266 | func TestCommand_listWhenNoHost(t *testing.T) { 267 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 268 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 269 | defer os.Remove("ngrpc.json") 270 | defer os.Remove("tsconfig.json") 271 | 272 | cmd := exec.Command("go", "run", "../cli/ngrpc/main.go", "list") 273 | out := string(goext.Ok(cmd.Output())) 274 | lines := strings.Split(out, "\n") 275 | 276 | assert.Equal(t, 277 | []string{"App", "URL", "Status", "Pid", "Uptime", "Memory", "CPU"}, 278 | strings.Fields(lines[0])) 279 | assert.Equal(t, 280 | []string{"example-server", "grpc://localhost:4000", "stopped", "N/A", "N/A", "N/A", "N/A"}, 281 | strings.Fields(lines[1])) 282 | assert.Equal(t, 283 | []string{"user-server", "grpcs://localhost:4001", "stopped", "N/A", "N/A", "N/A", "N/A"}, 284 | strings.Fields(lines[2])) 285 | assert.Equal(t, 286 | []string{"post-server", "grpcs://localhost:4002", "stopped", "N/A", "N/A", "N/A", "N/A"}, 287 | strings.Fields(lines[3])) 288 | } 289 | -------------------------------------------------------------------------------- /pm/socket/unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package socket 5 | 6 | import ( 7 | "net" 8 | "time" 9 | ) 10 | 11 | func Listen(path string) (net.Listener, error) { 12 | return net.Listen("unix", path) 13 | } 14 | 15 | func DialTimeout(path string, duration time.Duration) (net.Conn, error) { 16 | return net.DialTimeout("unix", path, duration) 17 | } 18 | -------------------------------------------------------------------------------- /pm/socket/windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package socket 5 | 6 | import ( 7 | "net" 8 | "time" 9 | 10 | "github.com/Microsoft/go-winio" 11 | ) 12 | 13 | func Listen(path string) (net.Listener, error) { 14 | return winio.ListenPipe(path, &winio.PipeConfig{}) 15 | } 16 | 17 | func DialTimeout(path string, duration time.Duration) (net.Conn, error) { 18 | second := time.Second 19 | return winio.DialPipe(path, &second) 20 | } 21 | -------------------------------------------------------------------------------- /pm/unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package pm 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "syscall" 10 | "testing" 11 | "time" 12 | 13 | "github.com/ayonli/goext" 14 | "github.com/ayonli/ngrpc/config" 15 | "github.com/ayonli/ngrpc/util" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestHost_WaitForExit(t *testing.T) { 20 | goext.Ok(0, util.CopyFile("../ngrpc.json", "ngrpc.json")) 21 | goext.Ok(0, util.CopyFile("../tsconfig.json", "tsconfig.json")) 22 | defer os.Remove("ngrpc.json") 23 | defer os.Remove("tsconfig.json") 24 | 25 | config := goext.Ok(config.LoadConfig()) 26 | host := NewHost(config, false) 27 | 28 | go func() { 29 | time.Sleep(time.Millisecond * 10) // wait a while for the host to start 30 | assert.Equal(t, 1, host.state) 31 | syscall.Kill(syscall.Getpid(), syscall.SIGINT) 32 | }() 33 | 34 | defer func() { 35 | if re := recover(); re != nil { 36 | assert.Equal(t, 0, host.state) 37 | assert.Equal(t, "unexpected call to os.Exit(0) during test", fmt.Sprint(re)) 38 | } 39 | }() 40 | 41 | host.Start(true) 42 | } 43 | -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | const { default: ngrpc } = require("@ayonli/ngrpc"); 2 | 3 | module.exports = ngrpc.loadConfigForPM2(); 4 | -------------------------------------------------------------------------------- /proto/ExampleService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./proto"; 4 | 5 | package services; 6 | 7 | message HelloRequest { 8 | string name = 1; 9 | } 10 | 11 | message HelloReply { 12 | string message = 2; 13 | } 14 | 15 | service ExampleService { 16 | rpc SayHello(HelloRequest) returns (HelloReply) {} 17 | } 18 | -------------------------------------------------------------------------------- /proto/github/ayonli/ngrpc/services/PostService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github/ayonli/ngrpc/services_proto"; 4 | 5 | package github.ayonli.ngrpc.services; 6 | 7 | import "github/ayonli/ngrpc/services/struct.proto"; 8 | 9 | message PostQuery { 10 | int32 id = 1; 11 | } 12 | 13 | message PostsQuery { 14 | optional string author = 1; 15 | optional string keyword = 2; 16 | } 17 | 18 | message PostSearchResult { 19 | repeated Post posts = 1; 20 | } 21 | 22 | service PostService { 23 | rpc GetPost(PostQuery) returns (Post) {} 24 | rpc SearchPosts(PostsQuery) returns (PostSearchResult) {} 25 | } 26 | -------------------------------------------------------------------------------- /proto/github/ayonli/ngrpc/services/UserService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github/ayonli/ngrpc/services_proto"; 4 | 5 | package github.ayonli.ngrpc.services; 6 | 7 | import "github/ayonli/ngrpc/services/struct.proto"; 8 | import "github/ayonli/ngrpc/services/PostService.proto"; 9 | 10 | message UserQuery { 11 | optional string id = 1; 12 | optional string email = 2; 13 | } 14 | 15 | message UsersQuery { 16 | optional Gender gender = 1; 17 | optional int32 minAge = 2; 18 | optional int32 maxAge = 3; 19 | } 20 | 21 | message UserQueryResult { 22 | repeated User users = 1; 23 | } 24 | 25 | service UserService { 26 | rpc GetUser(UserQuery) returns (User) {} 27 | rpc GetUsers(UsersQuery) returns (UserQueryResult) {} 28 | rpc GetMyPosts(UserQuery) returns (PostSearchResult) {} 29 | } 30 | -------------------------------------------------------------------------------- /proto/github/ayonli/ngrpc/services/struct.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github/ayonli/ngrpc/services_proto"; 4 | 5 | package github.ayonli.ngrpc.services; 6 | 7 | enum Gender { 8 | UNKNOWN = 0; 9 | MALE = 1; 10 | FEMALE = 2; 11 | } 12 | 13 | message User { 14 | string id = 1; 15 | string name = 2; 16 | Gender gender = 3; 17 | int32 age = 4; 18 | string email = 5; 19 | } 20 | 21 | message Post { 22 | int32 id = 1; 23 | string title = 2; 24 | optional string description = 3; 25 | string content = 4; 26 | optional User author = 5; 27 | } 28 | -------------------------------------------------------------------------------- /scripts/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ayonli/goext" 8 | "github.com/ayonli/ngrpc" 9 | "github.com/ayonli/ngrpc/services" 10 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 11 | "github.com/ayonli/ngrpc/services/proto" 12 | ) 13 | 14 | func main() { 15 | done := ngrpc.ForSnippet() 16 | defer done() 17 | 18 | ctx := context.Background() 19 | userId := "ayon.li" 20 | userSrv := goext.Ok((&services.UserService{}).GetClient(userId)) 21 | 22 | user := goext.Ok(userSrv.GetUser(ctx, &services_proto.UserQuery{Id: &userId})) 23 | fmt.Println(user) 24 | 25 | posts := goext.Ok(userSrv.GetMyPosts(ctx, &services_proto.UserQuery{Id: &userId})) 26 | fmt.Println(posts) 27 | 28 | expSrv := goext.Ok((&services.ExampleService{}).GetClient("")) 29 | reply := goext.Ok(expSrv.SayHello(ctx, &proto.HelloRequest{Name: "World"})) 30 | fmt.Println(reply.Message) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/main.ts: -------------------------------------------------------------------------------- 1 | import ngrpc from "@ayonli/ngrpc"; 2 | 3 | ngrpc.runSnippet(async () => { 4 | const userId = "ayon.li"; 5 | 6 | const user = await services.UserService.getUser({ id: userId }); 7 | console.log(user); 8 | 9 | const posts = await services.UserService.getMyPosts({ id: userId }); 10 | console.log(posts); 11 | 12 | const reply = await services.ExampleService.sayHello({ name: "World" }); 13 | console.log(reply.message); 14 | 15 | process.exit(0); // do not wait for idle 16 | }); 17 | -------------------------------------------------------------------------------- /services/ExampleService.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ayonli/ngrpc" 7 | "github.com/ayonli/ngrpc/services/proto" 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | type ExampleService struct { 12 | proto.UnimplementedExampleServiceServer 13 | } 14 | 15 | func (self *ExampleService) Serve(s grpc.ServiceRegistrar) { 16 | proto.RegisterExampleServiceServer(s, self) 17 | } 18 | 19 | func (self *ExampleService) Connect(cc grpc.ClientConnInterface) proto.ExampleServiceClient { 20 | return proto.NewExampleServiceClient(cc) 21 | } 22 | 23 | func (self *ExampleService) GetClient(route string) (proto.ExampleServiceClient, error) { 24 | return ngrpc.GetServiceClient(self, route) 25 | } 26 | 27 | func (self *ExampleService) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) { 28 | return &proto.HelloReply{Message: "Hello, " + req.Name}, nil 29 | } 30 | 31 | func init() { 32 | ngrpc.Use(&ExampleService{}) 33 | } 34 | -------------------------------------------------------------------------------- /services/ExampleService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClient, service } from "@ayonli/ngrpc"; 2 | 3 | declare global { 4 | namespace services { 5 | const ExampleService: ServiceClient; 6 | } 7 | } 8 | 9 | export type HelloRequest = { 10 | name: string; 11 | }; 12 | 13 | export type HelloReply = { 14 | message: string; 15 | }; 16 | 17 | @service("services.ExampleService") 18 | export default class ExampleService { 19 | async sayHello(req: HelloRequest): Promise { 20 | return await Promise.resolve({ message: "Hello, " + req.name }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/PostService.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/ayonli/ngrpc" 5 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | type PostService struct{} 10 | 11 | func (self *PostService) Connect(cc grpc.ClientConnInterface) services_proto.PostServiceClient { 12 | return services_proto.NewPostServiceClient(cc) 13 | } 14 | 15 | func (self *PostService) GetClient(route string) (services_proto.PostServiceClient, error) { 16 | return ngrpc.GetServiceClient(self, route) 17 | } 18 | 19 | func init() { 20 | ngrpc.Use(&PostService{}) 21 | } 22 | -------------------------------------------------------------------------------- /services/PostService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClient, LifecycleSupportInterface, service } from "@ayonli/ngrpc"; 2 | import { Post, User } from "./struct"; 3 | 4 | declare global { 5 | namespace services { 6 | const PostService: ServiceClient; 7 | } 8 | } 9 | 10 | export type PostQuery = { 11 | id: number; 12 | }; 13 | 14 | export type PostsQuery = { 15 | author?: string; 16 | keyword?: string; 17 | }; 18 | 19 | export type PostSearchResult = { 20 | posts: Post[]; 21 | }; 22 | 23 | @service("github.ayonli.ngrpc.services.PostService") 24 | export default class PostService implements LifecycleSupportInterface { 25 | private userSrv = services.UserService; 26 | private postStore: (Omit & { author: string; })[] | null = null; 27 | 28 | async init(): Promise { 29 | this.postStore = [ 30 | { 31 | id: 1, 32 | title: "My first article", 33 | description: "This is my first article", 34 | content: "The article contents ...", 35 | author: "ayon.li", 36 | }, 37 | { 38 | id: 2, 39 | title: "My second article", 40 | description: "This is my second article", 41 | content: "The article contents ...", 42 | author: "ayon.li", 43 | } 44 | ]; 45 | } 46 | 47 | async destroy(): Promise { 48 | this.postStore = null; 49 | } 50 | 51 | async getPost(query: PostQuery): Promise { 52 | const post = this.postStore?.find(item => item.id === query.id); 53 | 54 | if (post) { 55 | const author = await this.userSrv.getUser({ id: post.author }); 56 | return { ...post, author }; 57 | } else { 58 | throw new Error(`Post ${query.id} not found`); 59 | } 60 | } 61 | 62 | async searchPosts(query: PostsQuery): Promise { 63 | if (query.author) { 64 | const _posts = this.postStore?.filter(item => item.author === query.author); 65 | 66 | if (_posts?.length) { 67 | const { users } = await this.userSrv.getUsers({}); 68 | return { 69 | posts: _posts.map(post => { 70 | const author = users.find(user => user.id === post.author) as User; 71 | return { ...post, author }; 72 | }), 73 | }; 74 | } else { 75 | return { posts: [] }; 76 | } 77 | } else if (query.keyword) { 78 | const keywords = query.keyword.split(/\s+/); 79 | const _posts = this.postStore?.filter(post => { 80 | return keywords.some(keyword => post.title.includes(keyword)); 81 | }); 82 | 83 | if (_posts?.length) { 84 | const { users } = await this.userSrv.getUsers({}); 85 | return { 86 | posts: _posts.map(post => { 87 | const author = users.find(user => user.id === post.author) as User; 88 | return { ...post, author }; 89 | }), 90 | }; 91 | } else { 92 | return { posts: [] }; 93 | } 94 | } else { 95 | return { posts: [] }; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /services/UserService.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "slices" 7 | 8 | "github.com/ayonli/goext" 9 | "github.com/ayonli/goext/slicex" 10 | "github.com/ayonli/ngrpc" 11 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | type UserService struct { 16 | services_proto.UnimplementedUserServiceServer 17 | userStore []*services_proto.User 18 | PostSrv *PostService // set as exported field for dependency injection 19 | } 20 | 21 | func (self *UserService) Serve(s grpc.ServiceRegistrar) { 22 | services_proto.RegisterUserServiceServer(s, self) 23 | 24 | self.userStore = []*services_proto.User{ 25 | { 26 | Id: "ayon.li", 27 | Name: "A-yon Lee", 28 | Gender: services_proto.Gender_MALE, 29 | Age: 28, 30 | Email: "the@ayon.li", 31 | }, 32 | { 33 | Id: "john.doe", 34 | Name: "John Doe", 35 | Gender: services_proto.Gender_UNKNOWN, 36 | Age: -1, 37 | Email: "john.doe@example.com", 38 | }, 39 | } 40 | } 41 | 42 | func (self *UserService) Stop() { 43 | self.userStore = nil 44 | self.PostSrv = nil 45 | } 46 | 47 | func (self *UserService) Connect(cc grpc.ClientConnInterface) services_proto.UserServiceClient { 48 | return services_proto.NewUserServiceClient(cc) 49 | } 50 | 51 | func (self *UserService) GetClient(route string) (services_proto.UserServiceClient, error) { 52 | return ngrpc.GetServiceClient(self, route) 53 | } 54 | 55 | func (self *UserService) GetUser(ctx context.Context, query *services_proto.UserQuery) (*services_proto.User, error) { 56 | if query.Id != nil { 57 | idx := slices.IndexFunc[[]*services_proto.User](self.userStore, func(u *services_proto.User) bool { 58 | return u.Id == *query.Id 59 | }) 60 | 61 | if idx != -1 { 62 | return self.userStore[idx], nil 63 | } else { 64 | return nil, fmt.Errorf("user '%s' not found", *query.Id) 65 | } 66 | } else if query.Email != nil { 67 | idx := slices.IndexFunc[[]*services_proto.User](self.userStore, func(u *services_proto.User) bool { 68 | return u.Email == *query.Email 69 | }) 70 | 71 | if idx != -1 { 72 | return self.userStore[idx], nil 73 | } else { 74 | return nil, fmt.Errorf("user of '%s' not found", *query.Email) 75 | } 76 | } else { 77 | return nil, fmt.Errorf("one of the 'id' and 'email' must be provided") 78 | } 79 | } 80 | 81 | func (self *UserService) GetUsers( 82 | ctx context.Context, 83 | query *services_proto.UsersQuery, 84 | ) (*services_proto.UserQueryResult, error) { 85 | users := self.userStore 86 | 87 | if query.Gender != nil { 88 | users = slicex.Filter(users, func(user *services_proto.User, _ int) bool { 89 | return user.Gender == *query.Gender 90 | }) 91 | } 92 | 93 | if query.MinAge != nil { 94 | users = slicex.Filter(users, func(user *services_proto.User, _ int) bool { 95 | return user.Age >= *query.MinAge 96 | }) 97 | } 98 | 99 | if query.MaxAge != nil && *query.MaxAge != 0 { 100 | users = slicex.Filter(users, func(user *services_proto.User, _ int) bool { 101 | return user.Age <= *query.MaxAge 102 | }) 103 | } 104 | 105 | return &services_proto.UserQueryResult{Users: users}, nil 106 | } 107 | 108 | func (self *UserService) GetMyPosts( 109 | ctx context.Context, 110 | query *services_proto.UserQuery, 111 | ) (*services_proto.PostSearchResult, error) { 112 | return goext.Try(func() *services_proto.PostSearchResult { 113 | user := goext.Ok(self.GetUser(ctx, query)) 114 | ins := goext.Ok(self.PostSrv.GetClient(user.Id)) 115 | result := goext.Ok(ins.SearchPosts(ctx, &services_proto.PostsQuery{Author: &user.Id})) 116 | 117 | return (*services_proto.PostSearchResult)(result) 118 | }) 119 | } 120 | 121 | func init() { 122 | ngrpc.Use(&UserService{}) 123 | } 124 | -------------------------------------------------------------------------------- /services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClient, service } from "@ayonli/ngrpc"; 2 | import { Gender, User } from "./struct"; 3 | import { PostSearchResult } from "./PostService"; 4 | 5 | declare global { 6 | namespace services { 7 | const UserService: ServiceClient; 8 | } 9 | } 10 | 11 | export type UserQuery = { 12 | id?: string; 13 | email?: string; 14 | }; 15 | 16 | export type UsersQuery = { 17 | gender?: Gender; 18 | minAge?: number; 19 | maxAge?: number; 20 | }; 21 | 22 | export type UserQueryResult = { 23 | users: User[]; 24 | }; 25 | 26 | @service("github.ayonli.ngrpc.services.UserService") 27 | export default abstract class UserService { 28 | abstract getUser(query: UserQuery): Promise; 29 | abstract getUsers(query: UsersQuery): Promise; 30 | abstract getMyPosts(query: UserQuery): Promise; 31 | } 32 | -------------------------------------------------------------------------------- /services/github/ayonli/ngrpc/services_proto/PostService_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v4.23.4 5 | // source: github/ayonli/ngrpc/services/PostService.proto 6 | 7 | package services_proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | PostService_GetPost_FullMethodName = "/github.ayonli.ngrpc.services.PostService/GetPost" 23 | PostService_SearchPosts_FullMethodName = "/github.ayonli.ngrpc.services.PostService/SearchPosts" 24 | ) 25 | 26 | // PostServiceClient is the client API for PostService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | type PostServiceClient interface { 30 | GetPost(ctx context.Context, in *PostQuery, opts ...grpc.CallOption) (*Post, error) 31 | SearchPosts(ctx context.Context, in *PostsQuery, opts ...grpc.CallOption) (*PostSearchResult, error) 32 | } 33 | 34 | type postServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewPostServiceClient(cc grpc.ClientConnInterface) PostServiceClient { 39 | return &postServiceClient{cc} 40 | } 41 | 42 | func (c *postServiceClient) GetPost(ctx context.Context, in *PostQuery, opts ...grpc.CallOption) (*Post, error) { 43 | out := new(Post) 44 | err := c.cc.Invoke(ctx, PostService_GetPost_FullMethodName, in, out, opts...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return out, nil 49 | } 50 | 51 | func (c *postServiceClient) SearchPosts(ctx context.Context, in *PostsQuery, opts ...grpc.CallOption) (*PostSearchResult, error) { 52 | out := new(PostSearchResult) 53 | err := c.cc.Invoke(ctx, PostService_SearchPosts_FullMethodName, in, out, opts...) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return out, nil 58 | } 59 | 60 | // PostServiceServer is the server API for PostService service. 61 | // All implementations must embed UnimplementedPostServiceServer 62 | // for forward compatibility 63 | type PostServiceServer interface { 64 | GetPost(context.Context, *PostQuery) (*Post, error) 65 | SearchPosts(context.Context, *PostsQuery) (*PostSearchResult, error) 66 | mustEmbedUnimplementedPostServiceServer() 67 | } 68 | 69 | // UnimplementedPostServiceServer must be embedded to have forward compatible implementations. 70 | type UnimplementedPostServiceServer struct { 71 | } 72 | 73 | func (UnimplementedPostServiceServer) GetPost(context.Context, *PostQuery) (*Post, error) { 74 | return nil, status.Errorf(codes.Unimplemented, "method GetPost not implemented") 75 | } 76 | func (UnimplementedPostServiceServer) SearchPosts(context.Context, *PostsQuery) (*PostSearchResult, error) { 77 | return nil, status.Errorf(codes.Unimplemented, "method SearchPosts not implemented") 78 | } 79 | func (UnimplementedPostServiceServer) mustEmbedUnimplementedPostServiceServer() {} 80 | 81 | // UnsafePostServiceServer may be embedded to opt out of forward compatibility for this service. 82 | // Use of this interface is not recommended, as added methods to PostServiceServer will 83 | // result in compilation errors. 84 | type UnsafePostServiceServer interface { 85 | mustEmbedUnimplementedPostServiceServer() 86 | } 87 | 88 | func RegisterPostServiceServer(s grpc.ServiceRegistrar, srv PostServiceServer) { 89 | s.RegisterService(&PostService_ServiceDesc, srv) 90 | } 91 | 92 | func _PostService_GetPost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 93 | in := new(PostQuery) 94 | if err := dec(in); err != nil { 95 | return nil, err 96 | } 97 | if interceptor == nil { 98 | return srv.(PostServiceServer).GetPost(ctx, in) 99 | } 100 | info := &grpc.UnaryServerInfo{ 101 | Server: srv, 102 | FullMethod: PostService_GetPost_FullMethodName, 103 | } 104 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 105 | return srv.(PostServiceServer).GetPost(ctx, req.(*PostQuery)) 106 | } 107 | return interceptor(ctx, in, info, handler) 108 | } 109 | 110 | func _PostService_SearchPosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 111 | in := new(PostsQuery) 112 | if err := dec(in); err != nil { 113 | return nil, err 114 | } 115 | if interceptor == nil { 116 | return srv.(PostServiceServer).SearchPosts(ctx, in) 117 | } 118 | info := &grpc.UnaryServerInfo{ 119 | Server: srv, 120 | FullMethod: PostService_SearchPosts_FullMethodName, 121 | } 122 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 123 | return srv.(PostServiceServer).SearchPosts(ctx, req.(*PostsQuery)) 124 | } 125 | return interceptor(ctx, in, info, handler) 126 | } 127 | 128 | // PostService_ServiceDesc is the grpc.ServiceDesc for PostService service. 129 | // It's only intended for direct use with grpc.RegisterService, 130 | // and not to be introspected or modified (even as a copy) 131 | var PostService_ServiceDesc = grpc.ServiceDesc{ 132 | ServiceName: "github.ayonli.ngrpc.services.PostService", 133 | HandlerType: (*PostServiceServer)(nil), 134 | Methods: []grpc.MethodDesc{ 135 | { 136 | MethodName: "GetPost", 137 | Handler: _PostService_GetPost_Handler, 138 | }, 139 | { 140 | MethodName: "SearchPosts", 141 | Handler: _PostService_SearchPosts_Handler, 142 | }, 143 | }, 144 | Streams: []grpc.StreamDesc{}, 145 | Metadata: "github/ayonli/ngrpc/services/PostService.proto", 146 | } 147 | -------------------------------------------------------------------------------- /services/github/ayonli/ngrpc/services_proto/UserService_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v4.23.4 5 | // source: github/ayonli/ngrpc/services/UserService.proto 6 | 7 | package services_proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | UserService_GetUser_FullMethodName = "/github.ayonli.ngrpc.services.UserService/GetUser" 23 | UserService_GetUsers_FullMethodName = "/github.ayonli.ngrpc.services.UserService/GetUsers" 24 | UserService_GetMyPosts_FullMethodName = "/github.ayonli.ngrpc.services.UserService/GetMyPosts" 25 | ) 26 | 27 | // UserServiceClient is the client API for UserService service. 28 | // 29 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 30 | type UserServiceClient interface { 31 | GetUser(ctx context.Context, in *UserQuery, opts ...grpc.CallOption) (*User, error) 32 | GetUsers(ctx context.Context, in *UsersQuery, opts ...grpc.CallOption) (*UserQueryResult, error) 33 | GetMyPosts(ctx context.Context, in *UserQuery, opts ...grpc.CallOption) (*PostSearchResult, error) 34 | } 35 | 36 | type userServiceClient struct { 37 | cc grpc.ClientConnInterface 38 | } 39 | 40 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 41 | return &userServiceClient{cc} 42 | } 43 | 44 | func (c *userServiceClient) GetUser(ctx context.Context, in *UserQuery, opts ...grpc.CallOption) (*User, error) { 45 | out := new(User) 46 | err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return out, nil 51 | } 52 | 53 | func (c *userServiceClient) GetUsers(ctx context.Context, in *UsersQuery, opts ...grpc.CallOption) (*UserQueryResult, error) { 54 | out := new(UserQueryResult) 55 | err := c.cc.Invoke(ctx, UserService_GetUsers_FullMethodName, in, out, opts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | func (c *userServiceClient) GetMyPosts(ctx context.Context, in *UserQuery, opts ...grpc.CallOption) (*PostSearchResult, error) { 63 | out := new(PostSearchResult) 64 | err := c.cc.Invoke(ctx, UserService_GetMyPosts_FullMethodName, in, out, opts...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return out, nil 69 | } 70 | 71 | // UserServiceServer is the server API for UserService service. 72 | // All implementations must embed UnimplementedUserServiceServer 73 | // for forward compatibility 74 | type UserServiceServer interface { 75 | GetUser(context.Context, *UserQuery) (*User, error) 76 | GetUsers(context.Context, *UsersQuery) (*UserQueryResult, error) 77 | GetMyPosts(context.Context, *UserQuery) (*PostSearchResult, error) 78 | mustEmbedUnimplementedUserServiceServer() 79 | } 80 | 81 | // UnimplementedUserServiceServer must be embedded to have forward compatible implementations. 82 | type UnimplementedUserServiceServer struct { 83 | } 84 | 85 | func (UnimplementedUserServiceServer) GetUser(context.Context, *UserQuery) (*User, error) { 86 | return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") 87 | } 88 | func (UnimplementedUserServiceServer) GetUsers(context.Context, *UsersQuery) (*UserQueryResult, error) { 89 | return nil, status.Errorf(codes.Unimplemented, "method GetUsers not implemented") 90 | } 91 | func (UnimplementedUserServiceServer) GetMyPosts(context.Context, *UserQuery) (*PostSearchResult, error) { 92 | return nil, status.Errorf(codes.Unimplemented, "method GetMyPosts not implemented") 93 | } 94 | func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} 95 | 96 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 97 | // Use of this interface is not recommended, as added methods to UserServiceServer will 98 | // result in compilation errors. 99 | type UnsafeUserServiceServer interface { 100 | mustEmbedUnimplementedUserServiceServer() 101 | } 102 | 103 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 104 | s.RegisterService(&UserService_ServiceDesc, srv) 105 | } 106 | 107 | func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 108 | in := new(UserQuery) 109 | if err := dec(in); err != nil { 110 | return nil, err 111 | } 112 | if interceptor == nil { 113 | return srv.(UserServiceServer).GetUser(ctx, in) 114 | } 115 | info := &grpc.UnaryServerInfo{ 116 | Server: srv, 117 | FullMethod: UserService_GetUser_FullMethodName, 118 | } 119 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 120 | return srv.(UserServiceServer).GetUser(ctx, req.(*UserQuery)) 121 | } 122 | return interceptor(ctx, in, info, handler) 123 | } 124 | 125 | func _UserService_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 126 | in := new(UsersQuery) 127 | if err := dec(in); err != nil { 128 | return nil, err 129 | } 130 | if interceptor == nil { 131 | return srv.(UserServiceServer).GetUsers(ctx, in) 132 | } 133 | info := &grpc.UnaryServerInfo{ 134 | Server: srv, 135 | FullMethod: UserService_GetUsers_FullMethodName, 136 | } 137 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 138 | return srv.(UserServiceServer).GetUsers(ctx, req.(*UsersQuery)) 139 | } 140 | return interceptor(ctx, in, info, handler) 141 | } 142 | 143 | func _UserService_GetMyPosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 144 | in := new(UserQuery) 145 | if err := dec(in); err != nil { 146 | return nil, err 147 | } 148 | if interceptor == nil { 149 | return srv.(UserServiceServer).GetMyPosts(ctx, in) 150 | } 151 | info := &grpc.UnaryServerInfo{ 152 | Server: srv, 153 | FullMethod: UserService_GetMyPosts_FullMethodName, 154 | } 155 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 156 | return srv.(UserServiceServer).GetMyPosts(ctx, req.(*UserQuery)) 157 | } 158 | return interceptor(ctx, in, info, handler) 159 | } 160 | 161 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 162 | // It's only intended for direct use with grpc.RegisterService, 163 | // and not to be introspected or modified (even as a copy) 164 | var UserService_ServiceDesc = grpc.ServiceDesc{ 165 | ServiceName: "github.ayonli.ngrpc.services.UserService", 166 | HandlerType: (*UserServiceServer)(nil), 167 | Methods: []grpc.MethodDesc{ 168 | { 169 | MethodName: "GetUser", 170 | Handler: _UserService_GetUser_Handler, 171 | }, 172 | { 173 | MethodName: "GetUsers", 174 | Handler: _UserService_GetUsers_Handler, 175 | }, 176 | { 177 | MethodName: "GetMyPosts", 178 | Handler: _UserService_GetMyPosts_Handler, 179 | }, 180 | }, 181 | Streams: []grpc.StreamDesc{}, 182 | Metadata: "github/ayonli/ngrpc/services/UserService.proto", 183 | } 184 | -------------------------------------------------------------------------------- /services/proto/ExampleService.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.23.4 5 | // source: ExampleService.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type HelloRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 29 | } 30 | 31 | func (x *HelloRequest) Reset() { 32 | *x = HelloRequest{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_ExampleService_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *HelloRequest) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*HelloRequest) ProtoMessage() {} 45 | 46 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 47 | mi := &file_ExampleService_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 59 | func (*HelloRequest) Descriptor() ([]byte, []int) { 60 | return file_ExampleService_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *HelloRequest) GetName() string { 64 | if x != nil { 65 | return x.Name 66 | } 67 | return "" 68 | } 69 | 70 | type HelloReply struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | 75 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 76 | } 77 | 78 | func (x *HelloReply) Reset() { 79 | *x = HelloReply{} 80 | if protoimpl.UnsafeEnabled { 81 | mi := &file_ExampleService_proto_msgTypes[1] 82 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 83 | ms.StoreMessageInfo(mi) 84 | } 85 | } 86 | 87 | func (x *HelloReply) String() string { 88 | return protoimpl.X.MessageStringOf(x) 89 | } 90 | 91 | func (*HelloReply) ProtoMessage() {} 92 | 93 | func (x *HelloReply) ProtoReflect() protoreflect.Message { 94 | mi := &file_ExampleService_proto_msgTypes[1] 95 | if protoimpl.UnsafeEnabled && x != nil { 96 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 97 | if ms.LoadMessageInfo() == nil { 98 | ms.StoreMessageInfo(mi) 99 | } 100 | return ms 101 | } 102 | return mi.MessageOf(x) 103 | } 104 | 105 | // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. 106 | func (*HelloReply) Descriptor() ([]byte, []int) { 107 | return file_ExampleService_proto_rawDescGZIP(), []int{1} 108 | } 109 | 110 | func (x *HelloReply) GetMessage() string { 111 | if x != nil { 112 | return x.Message 113 | } 114 | return "" 115 | } 116 | 117 | var File_ExampleService_proto protoreflect.FileDescriptor 118 | 119 | var file_ExampleService_proto_rawDesc = []byte{ 120 | 0x0a, 0x14, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 121 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 122 | 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 123 | 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 124 | 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 125 | 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 126 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x4c, 0x0a, 0x0e, 127 | 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 128 | 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x16, 0x2e, 0x73, 0x65, 0x72, 129 | 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 130 | 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x48, 0x65, 131 | 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 132 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 133 | } 134 | 135 | var ( 136 | file_ExampleService_proto_rawDescOnce sync.Once 137 | file_ExampleService_proto_rawDescData = file_ExampleService_proto_rawDesc 138 | ) 139 | 140 | func file_ExampleService_proto_rawDescGZIP() []byte { 141 | file_ExampleService_proto_rawDescOnce.Do(func() { 142 | file_ExampleService_proto_rawDescData = protoimpl.X.CompressGZIP(file_ExampleService_proto_rawDescData) 143 | }) 144 | return file_ExampleService_proto_rawDescData 145 | } 146 | 147 | var file_ExampleService_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 148 | var file_ExampleService_proto_goTypes = []interface{}{ 149 | (*HelloRequest)(nil), // 0: services.HelloRequest 150 | (*HelloReply)(nil), // 1: services.HelloReply 151 | } 152 | var file_ExampleService_proto_depIdxs = []int32{ 153 | 0, // 0: services.ExampleService.SayHello:input_type -> services.HelloRequest 154 | 1, // 1: services.ExampleService.SayHello:output_type -> services.HelloReply 155 | 1, // [1:2] is the sub-list for method output_type 156 | 0, // [0:1] is the sub-list for method input_type 157 | 0, // [0:0] is the sub-list for extension type_name 158 | 0, // [0:0] is the sub-list for extension extendee 159 | 0, // [0:0] is the sub-list for field type_name 160 | } 161 | 162 | func init() { file_ExampleService_proto_init() } 163 | func file_ExampleService_proto_init() { 164 | if File_ExampleService_proto != nil { 165 | return 166 | } 167 | if !protoimpl.UnsafeEnabled { 168 | file_ExampleService_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 169 | switch v := v.(*HelloRequest); i { 170 | case 0: 171 | return &v.state 172 | case 1: 173 | return &v.sizeCache 174 | case 2: 175 | return &v.unknownFields 176 | default: 177 | return nil 178 | } 179 | } 180 | file_ExampleService_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 181 | switch v := v.(*HelloReply); i { 182 | case 0: 183 | return &v.state 184 | case 1: 185 | return &v.sizeCache 186 | case 2: 187 | return &v.unknownFields 188 | default: 189 | return nil 190 | } 191 | } 192 | } 193 | type x struct{} 194 | out := protoimpl.TypeBuilder{ 195 | File: protoimpl.DescBuilder{ 196 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 197 | RawDescriptor: file_ExampleService_proto_rawDesc, 198 | NumEnums: 0, 199 | NumMessages: 2, 200 | NumExtensions: 0, 201 | NumServices: 1, 202 | }, 203 | GoTypes: file_ExampleService_proto_goTypes, 204 | DependencyIndexes: file_ExampleService_proto_depIdxs, 205 | MessageInfos: file_ExampleService_proto_msgTypes, 206 | }.Build() 207 | File_ExampleService_proto = out.File 208 | file_ExampleService_proto_rawDesc = nil 209 | file_ExampleService_proto_goTypes = nil 210 | file_ExampleService_proto_depIdxs = nil 211 | } 212 | -------------------------------------------------------------------------------- /services/proto/ExampleService_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v4.23.4 5 | // source: ExampleService.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | ExampleService_SayHello_FullMethodName = "/services.ExampleService/SayHello" 23 | ) 24 | 25 | // ExampleServiceClient is the client API for ExampleService service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type ExampleServiceClient interface { 29 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) 30 | } 31 | 32 | type exampleServiceClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewExampleServiceClient(cc grpc.ClientConnInterface) ExampleServiceClient { 37 | return &exampleServiceClient{cc} 38 | } 39 | 40 | func (c *exampleServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 41 | out := new(HelloReply) 42 | err := c.cc.Invoke(ctx, ExampleService_SayHello_FullMethodName, in, out, opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return out, nil 47 | } 48 | 49 | // ExampleServiceServer is the server API for ExampleService service. 50 | // All implementations must embed UnimplementedExampleServiceServer 51 | // for forward compatibility 52 | type ExampleServiceServer interface { 53 | SayHello(context.Context, *HelloRequest) (*HelloReply, error) 54 | mustEmbedUnimplementedExampleServiceServer() 55 | } 56 | 57 | // UnimplementedExampleServiceServer must be embedded to have forward compatible implementations. 58 | type UnimplementedExampleServiceServer struct { 59 | } 60 | 61 | func (UnimplementedExampleServiceServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { 62 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 63 | } 64 | func (UnimplementedExampleServiceServer) mustEmbedUnimplementedExampleServiceServer() {} 65 | 66 | // UnsafeExampleServiceServer may be embedded to opt out of forward compatibility for this service. 67 | // Use of this interface is not recommended, as added methods to ExampleServiceServer will 68 | // result in compilation errors. 69 | type UnsafeExampleServiceServer interface { 70 | mustEmbedUnimplementedExampleServiceServer() 71 | } 72 | 73 | func RegisterExampleServiceServer(s grpc.ServiceRegistrar, srv ExampleServiceServer) { 74 | s.RegisterService(&ExampleService_ServiceDesc, srv) 75 | } 76 | 77 | func _ExampleService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 78 | in := new(HelloRequest) 79 | if err := dec(in); err != nil { 80 | return nil, err 81 | } 82 | if interceptor == nil { 83 | return srv.(ExampleServiceServer).SayHello(ctx, in) 84 | } 85 | info := &grpc.UnaryServerInfo{ 86 | Server: srv, 87 | FullMethod: ExampleService_SayHello_FullMethodName, 88 | } 89 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 90 | return srv.(ExampleServiceServer).SayHello(ctx, req.(*HelloRequest)) 91 | } 92 | return interceptor(ctx, in, info, handler) 93 | } 94 | 95 | // ExampleService_ServiceDesc is the grpc.ServiceDesc for ExampleService service. 96 | // It's only intended for direct use with grpc.RegisterService, 97 | // and not to be introspected or modified (even as a copy) 98 | var ExampleService_ServiceDesc = grpc.ServiceDesc{ 99 | ServiceName: "services.ExampleService", 100 | HandlerType: (*ExampleServiceServer)(nil), 101 | Methods: []grpc.MethodDesc{ 102 | { 103 | MethodName: "SayHello", 104 | Handler: _ExampleService_SayHello_Handler, 105 | }, 106 | }, 107 | Streams: []grpc.StreamDesc{}, 108 | Metadata: "ExampleService.proto", 109 | } 110 | -------------------------------------------------------------------------------- /services/struct.ts: -------------------------------------------------------------------------------- 1 | export enum Gender { 2 | UNKNOWN = 0, 3 | MALE = 1, 4 | FEMALE = 2, 5 | } 6 | 7 | export type User = { 8 | id: string; 9 | name: string; 10 | gender: Gender; 11 | age: number; 12 | email: string; 13 | }; 14 | 15 | export type Post = { 16 | id: number; 17 | title: string; 18 | description?: string; 19 | content: string; 20 | author: User | null; 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "es2018", 5 | "outDir": "./dist", 6 | "newLine": "LF", 7 | "incremental": true, 8 | "importHelpers": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "noUnusedParameters": true, 12 | "noUnusedLocals": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noImplicitOverride": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noPropertyAccessFromIndexSignature": true, 19 | "noUncheckedIndexedAccess": true, 20 | "skipLibCheck": true, 21 | "paths": { 22 | "@ayonli/ngrpc": [ 23 | "." 24 | ] 25 | }, 26 | }, 27 | "include": [ 28 | "*.ts", 29 | "*/**.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package ngrpc_test 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | "testing" 10 | 11 | "github.com/ayonli/ngrpc" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestWaitForExit(t *testing.T) { 16 | app, _ := ngrpc.Start("user-server") 17 | 18 | go func() { 19 | syscall.Kill(syscall.Getpid(), syscall.SIGINT) 20 | }() 21 | 22 | defer func() { 23 | if re := recover(); re != nil { 24 | assert.Equal(t, "unexpected call to os.Exit(0) during test", fmt.Sprint(re)) 25 | } 26 | }() 27 | 28 | app.WaitForExit() 29 | } 30 | -------------------------------------------------------------------------------- /util/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "mocha"; 2 | import * as assert from "node:assert"; 3 | import * as path from "node:path"; 4 | import { exists } from "@ayonli/jsext/fs"; 5 | import { absPath, service, timed } from "."; 6 | 7 | test("exists", async () => { 8 | const ok1 = await exists("ngrpc.json"); 9 | const ok2 = await exists("ngrpc.local.json"); 10 | 11 | assert.ok(ok1); 12 | assert.ok(!ok2); 13 | }); 14 | 15 | test("absPath", async () => { 16 | const file1 = absPath("./ngrpc.json"); 17 | const file2 = absPath("/usr/local/bin"); 18 | 19 | assert.strictEqual(file1, path.join(process.cwd(), "ngrpc.json")); 20 | assert.strictEqual(file2, "/usr/local/bin"); 21 | 22 | if (process.platform === "win32") { 23 | const filename = "C:\\Program Files\\nodejs\\bin"; 24 | const file3 = absPath(filename); 25 | assert.strictEqual(file3, filename); 26 | 27 | const file4 = absPath(filename, true); 28 | assert.strictEqual(file4, "\\\\.\\pipe\\" + filename); 29 | } 30 | }); 31 | 32 | test("timed", () => { 33 | const str = timed`everything is fine`; 34 | assert.ok(str.match(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} /)); 35 | assert.ok(str.endsWith(" everything is fine")); 36 | }); 37 | 38 | test("@service", () => { 39 | class Foo { } 40 | class Bar { } 41 | 42 | service("services.Foo")(Foo); 43 | service("services.Bar")(Bar, {}); 44 | 45 | // @ts-ignore 46 | assert.strictEqual(Foo[Symbol.for("serviceName")], "services.Foo"); 47 | // @ts-ignore 48 | assert.strictEqual(Bar[Symbol.for("serviceName")], "services.Bar"); 49 | }); 50 | -------------------------------------------------------------------------------- /util/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | 3 | export const sServiceName = Symbol.for("serviceName"); 4 | 5 | export function absPath(filename: string, withPipe = false): string { 6 | let isAbs = false; 7 | 8 | if (!/^\/|^[a-zA-Z]:[\\\/]/.test(filename)) { 9 | filename = path.resolve(process.cwd(), filename); 10 | } else { 11 | isAbs = true; 12 | } 13 | 14 | if (!isAbs && path.sep) { 15 | filename = filename.replace(/\\|\//g, path.sep); 16 | } 17 | 18 | if (withPipe && 19 | typeof process === "object" && process.platform === "win32" && 20 | !/^\\\\[.?]\\pipe\\/.test(filename) 21 | ) { 22 | filename = "\\\\.\\pipe\\" + filename; 23 | } 24 | 25 | return filename; 26 | } 27 | 28 | export function timed(callSite: TemplateStringsArray, ...bindings: any[]) { 29 | const text = callSite.map((str, i) => { 30 | return i > 0 ? bindings[i - 1] + str : str; 31 | }).join(""); 32 | const now = new Date(); 33 | const year = now.getFullYear(); 34 | const month = String(now.getMonth() + 1).padStart(2, "0"); 35 | const date = String(now.getDate()).padStart(2, "0"); 36 | const hours = String(now.getHours()).padStart(2, "0"); 37 | const minutes = String(now.getMinutes()).padStart(2, "0"); 38 | const seconds = String(now.getSeconds()).padStart(2, "0"); 39 | 40 | return `${year}/${month}/${date} ${hours}:${minutes}:${seconds} ${text}`; 41 | } 42 | 43 | /** 44 | * This decorator function is used to link the service class to a gRPC service. 45 | * 46 | * @param name The service name defined in the `.proto` file. 47 | */ 48 | export function service(name: string): any>( 49 | target: T, 50 | ctx?: any 51 | ) => void | T { 52 | return (target: any) => { 53 | target[sServiceName] = name; 54 | return target; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "hash/fnv" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/ayonli/goext" 16 | "github.com/ayonli/goext/slicex" 17 | "github.com/ayonli/goext/stringx" 18 | "github.com/struCoder/pidusage" 19 | ) 20 | 21 | func Exists(filename string) bool { 22 | if _, err := os.Stat(filename); err == nil { 23 | return true 24 | } else if errors.Is(err, os.ErrNotExist) { 25 | return false 26 | } else { 27 | panic(err) 28 | } 29 | } 30 | 31 | func AbsPath(filename string, pipePrefix bool) string { 32 | isAbs := false 33 | 34 | if stringx.Search(filename, `^/|^[a-zA-Z]:[\\/]`) == -1 { 35 | cwd, _ := os.Getwd() 36 | filename = filepath.Join(cwd, filename) 37 | } else { 38 | isAbs = true 39 | } 40 | 41 | if !isAbs { 42 | if filepath.Separator == '/' { 43 | filename = strings.ReplaceAll(filename, "\\", "/") 44 | } else if filepath.Separator == '\\' { 45 | filename = strings.ReplaceAll(filename, "/", "\\") 46 | } 47 | } 48 | 49 | if pipePrefix && runtime.GOOS == "windows" && stringx.Search(filename, "^\\\\[.?]\\pipe\\") == -1 { 50 | return "\\\\.\\pipe\\" + filename 51 | } else { 52 | return filename 53 | } 54 | } 55 | 56 | func Hash(str string) int { 57 | hash := fnv.New32() 58 | hash.Write([]byte(str)) 59 | return int(hash.Sum32()) 60 | } 61 | 62 | func EnsureDir(dirname string) error { 63 | if err := os.MkdirAll(dirname, 0755); err != nil { 64 | if errors.Is(err, os.ErrExist) { 65 | return nil 66 | } else { 67 | return err 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func CopyFile(src string, dst string) error { 75 | in, err := os.Open(src) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | out, err := os.Create(dst) 82 | 83 | if err != nil { 84 | return err 85 | } 86 | 87 | _, err = io.Copy(out, in) 88 | 89 | if err == nil { // must close the files for Windows 90 | in.Close() 91 | out.Close() 92 | } 93 | 94 | return err 95 | } 96 | 97 | func GetPidStat(pid int) (*pidusage.SysInfo, error) { 98 | return goext.Try(func() *pidusage.SysInfo { 99 | if runtime.GOOS == "windows" { 100 | cmd := exec.Command("powershell", "-nologo", "-noprofile") 101 | stdin := goext.Ok(cmd.StdinPipe()) 102 | 103 | go func() { 104 | defer stdin.Close() 105 | fmt.Fprint(stdin, "Get-Process -Id "+strconv.Itoa(pid)) 106 | }() 107 | 108 | out := goext.Ok(cmd.CombinedOutput()) 109 | lines := slicex.Map(strings.Split(string(out), "\n"), func(line string, _ int) string { 110 | return strings.Trim(line, "\r\n\t ") 111 | }) 112 | columns := strings.Fields(lines[3]) 113 | memory := goext.Ok(strconv.Atoi(columns[3])) 114 | cpu := goext.Ok(strconv.ParseFloat(columns[4], 64)) 115 | 116 | return &pidusage.SysInfo{ 117 | Memory: float64(memory) * 1024, 118 | CPU: cpu, 119 | } 120 | } else { 121 | return goext.Ok(pidusage.GetStat(pid)) 122 | } 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/ayonli/goext" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestExists(t *testing.T) { 15 | ok1 := Exists("../ngrpc.json") 16 | ok2 := Exists("../ngrpc.local.json") 17 | 18 | assert.True(t, ok1) 19 | assert.False(t, ok2) 20 | } 21 | 22 | func TestAbsPath(t *testing.T) { 23 | file1 := AbsPath("../ngrpc.sock", false) 24 | file2 := AbsPath("/usr/local/bin", false) 25 | cwd := goext.Ok(os.Getwd()) 26 | 27 | assert.Equal(t, filepath.Clean(cwd+"/../ngrpc.sock"), file1) 28 | assert.Equal(t, "/usr/local/bin", file2) 29 | 30 | if runtime.GOOS == "windows" { 31 | filename := "C:\\Program Files\\go\\bin" 32 | file3 := AbsPath(filename, false) 33 | assert.Equal(t, filename, file3) 34 | 35 | file4 := AbsPath(filename, true) 36 | assert.Equal(t, "\\\\.\\pipe\\"+filename, file4) 37 | } 38 | } 39 | 40 | func TestHash(t *testing.T) { 41 | hash := Hash("hello, world!") 42 | 43 | assert.Equal(t, 10, len(strconv.Itoa(hash))) 44 | } 45 | 46 | func TestEnsureDir(t *testing.T) { 47 | assert.False(t, Exists("test/foo/bar")) 48 | 49 | goext.Ok(0, EnsureDir("test/foo/bar")) 50 | 51 | assert.True(t, Exists("test/foo/bar")) 52 | 53 | goext.Ok(0, os.Remove("test/foo/bar")) 54 | goext.Ok(0, os.Remove("test/foo")) 55 | goext.Ok(0, os.Remove("test")) 56 | } 57 | 58 | func TestCopyFile(t *testing.T) { 59 | goext.Ok(0, CopyFile("../ngrpc.json", "ngrpc.json")) 60 | defer os.Remove("ngrpc.json") 61 | 62 | srcContents := goext.Ok(os.ReadFile("../ngrpc.json")) 63 | dstContents := goext.Ok(os.ReadFile("ngrpc.json")) 64 | 65 | assert.Equal(t, srcContents, dstContents) 66 | } 67 | 68 | func TestGetPidStat(t *testing.T) { 69 | stat := goext.Ok(GetPidStat(os.Getpid())) 70 | assert.True(t, stat.Memory > 0) 71 | assert.True(t, stat.CPU >= 0.00) 72 | } 73 | -------------------------------------------------------------------------------- /web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/ayonli/goext" 12 | "github.com/ayonli/ngrpc" 13 | "github.com/ayonli/ngrpc/services" 14 | "github.com/ayonli/ngrpc/services/github/ayonli/ngrpc/services_proto" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | func main() { 19 | appName := ngrpc.GetAppName() 20 | app := goext.Ok(ngrpc.Start(appName)) 21 | defer app.WaitForExit() 22 | 23 | var httpServer *http.Server 24 | var httpsServer *http.Server 25 | route := gin.Default() 26 | urlObj := goext.Ok(url.Parse(app.Url)) 27 | 28 | if urlObj.Scheme == "https" { 29 | port := urlObj.Port() 30 | 31 | if port == "" { 32 | port = "443" 33 | } 34 | 35 | httpsServer = &http.Server{ 36 | Addr: ":" + port, 37 | Handler: route, 38 | } 39 | } else { 40 | port := urlObj.Port() 41 | 42 | if port == "" { 43 | port = "80" 44 | } 45 | 46 | httpServer = &http.Server{ 47 | Addr: ":" + port, 48 | Handler: route, 49 | } 50 | } 51 | 52 | fmt.Println(httpServer, httpsServer) 53 | 54 | go func() { 55 | if httpsServer != nil { 56 | httpsServer.ListenAndServeTLS(app.Cert, app.Key) 57 | } else if httpServer != nil { 58 | httpServer.ListenAndServe() 59 | } 60 | }() 61 | 62 | app.OnStop(func() { 63 | if httpsServer != nil { 64 | goext.Ok(0, httpsServer.Shutdown(context.Background())) 65 | } else if httpServer != nil { 66 | goext.Ok(0, httpServer.Shutdown(context.Background())) 67 | } 68 | }) 69 | 70 | route.GET("/user/:id", func(ctx *gin.Context) { 71 | userId := ctx.Param("id") 72 | userSrv := goext.Ok((&services.UserService{}).GetClient(userId)) 73 | user, err := userSrv.GetUser(ctx, &services_proto.UserQuery{Id: &userId}) 74 | 75 | if err != nil { 76 | if strings.Contains(err.Error(), "not found") { 77 | ctx.PureJSON(200, gin.H{"code": 404, "error": err.Error()}) 78 | } else { 79 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 80 | } 81 | } else { 82 | ctx.PureJSON(200, gin.H{"code": 0, "data": user}) 83 | } 84 | }).GET("/users/gender/:gender", func(ctx *gin.Context) { 85 | var gender services_proto.Gender 86 | 87 | if ctx.Param("gender") == "unknown" { 88 | gender = services_proto.Gender_UNKNOWN 89 | } else if ctx.Param("gender") == "male" { 90 | gender = services_proto.Gender_MALE 91 | } else if ctx.Param("gender") == "female" { 92 | gender = services_proto.Gender_FEMALE 93 | } else { 94 | ctx.PureJSON(200, gin.H{ 95 | "code": 400, 96 | "error": "unrecognized gender argument, shall be either 'male', 'female' or 'unknown'", 97 | }) 98 | return 99 | } 100 | 101 | userSrv := goext.Ok((&services.UserService{}).GetClient(ctx.Param("gender"))) 102 | result, err := userSrv.GetUsers(ctx, &services_proto.UsersQuery{Gender: &gender}) 103 | 104 | if err != nil { 105 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 106 | } else { 107 | ctx.PureJSON(200, gin.H{"code": 0, "data": result.Users}) 108 | } 109 | }).GET("/users/age/:min/to/:max", func(ctx *gin.Context) { 110 | minAge, err1 := strconv.Atoi(ctx.Param("min")) 111 | maxAge, err2 := strconv.Atoi(ctx.Param("max")) 112 | 113 | if err1 != nil || err2 != nil { 114 | ctx.PureJSON(200, gin.H{"code": 400, "error": "unrecognized age range"}) 115 | return 116 | } 117 | 118 | _minAge := int32(minAge) 119 | _maxAge := int32(maxAge) 120 | userSrv := goext.Ok((&services.UserService{}).GetClient("")) 121 | result, err := userSrv.GetUsers(ctx, &services_proto.UsersQuery{ 122 | MinAge: &_minAge, 123 | MaxAge: &_maxAge, 124 | }) 125 | 126 | if err != nil { 127 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 128 | } else { 129 | ctx.PureJSON(200, gin.H{"code": 0, "data": result.Users}) 130 | } 131 | }).GET("/user/:id/posts", func(ctx *gin.Context) { 132 | userId := ctx.Param("id") 133 | userSrv := goext.Ok((&services.UserService{}).GetClient(userId)) 134 | result, err := userSrv.GetMyPosts(ctx, &services_proto.UserQuery{Id: &userId}) 135 | 136 | if err != nil { 137 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 138 | } else { 139 | ctx.PureJSON(200, gin.H{"code": 0, "data": result.Posts}) 140 | } 141 | }).GET("/post/:id", func(ctx *gin.Context) { 142 | idStr := ctx.Param("id") 143 | id, err := strconv.Atoi(idStr) 144 | 145 | if err != nil { 146 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 147 | return 148 | } 149 | 150 | postSrv := goext.Ok((&services.PostService{}).GetClient(idStr)) 151 | post, err := postSrv.GetPost(ctx, &services_proto.PostQuery{Id: int32(id)}) 152 | 153 | if err != nil { 154 | if strings.Contains(err.Error(), "not found") { 155 | ctx.PureJSON(200, gin.H{"code": 404, "error": err.Error()}) 156 | } else { 157 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 158 | } 159 | } else { 160 | ctx.PureJSON(200, gin.H{"code": 0, "data": post}) 161 | } 162 | }).GET("/posts/search/:keyword", func(ctx *gin.Context) { 163 | keyword := ctx.Param("keyword") 164 | postSrv := goext.Ok((&services.PostService{}).GetClient(keyword)) 165 | result, err := postSrv.SearchPosts(ctx, &services_proto.PostsQuery{ 166 | Keyword: &keyword, 167 | }) 168 | 169 | if err != nil { 170 | ctx.PureJSON(200, gin.H{"code": 500, "error": err.Error()}) 171 | } else { 172 | ctx.PureJSON(200, gin.H{"code": 0, "data": result.Posts}) 173 | } 174 | }) 175 | } 176 | -------------------------------------------------------------------------------- /web/main.ts: -------------------------------------------------------------------------------- 1 | import * as http from "node:http"; 2 | import * as https from "node:https"; 3 | import * as fs from "node:fs/promises"; 4 | import _try from "@ayonli/jsext/try"; 5 | import ngrpc from "@ayonli/ngrpc"; 6 | import express from "express"; 7 | import { Gender, Post, User } from "../services/struct"; 8 | 9 | type ApiResponse = { 10 | code: number; 11 | data?: T; 12 | error?: string; 13 | }; 14 | 15 | (async () => { 16 | const appName = ngrpc.getAppName(); 17 | const app = await ngrpc.start(appName); 18 | app.waitForExit(); 19 | 20 | if (app.name !== "web-server") { 21 | process.send?.("ready"); // for PM2 compatibility 22 | return; 23 | } 24 | 25 | let httpServer: http.Server; 26 | let httpsServer: https.Server; 27 | const route = express(); 28 | 29 | const startWebServer = async () => { 30 | const { protocol, port } = new URL(app.url); 31 | 32 | if (protocol === "https:") { 33 | httpsServer = https.createServer({ 34 | cert: await fs.readFile(app.cert as string), 35 | key: await fs.readFile(app.key as string), 36 | ca: app.ca ? await fs.readFile(app.ca as string) : undefined, 37 | }, route).listen(port || "443", () => { 38 | process.send?.("ready"); 39 | }); 40 | } else { 41 | httpServer = http.createServer(route).listen(port || "80", () => { 42 | process.send?.("ready"); 43 | }); 44 | } 45 | }; 46 | 47 | await startWebServer(); 48 | 49 | app.onStop(() => { 50 | httpServer?.close(); 51 | httpsServer?.close(); 52 | }); 53 | app.onReload(() => { 54 | // restart the web server with the newest configuration. 55 | httpServer?.close(startWebServer); 56 | httpsServer?.close(startWebServer); 57 | }); 58 | 59 | route.get("/user/:id", async (req, res) => { 60 | type UserResponse = ApiResponse; 61 | const userId = req.params.id; 62 | const [err, user] = await _try(services.UserService.getUser({ id: userId })); 63 | 64 | if (err instanceof Error) { 65 | if (err.message.includes("not found")) { 66 | res.json({ code: 404, error: err.message } satisfies UserResponse); 67 | } else { 68 | res.json({ code: 500, error: err.message } satisfies UserResponse); 69 | } 70 | } else { 71 | res.json({ code: 0, data: user } satisfies UserResponse); 72 | } 73 | }).get("/users/gender/:gender", async (req, res) => { 74 | type UsersResponse = ApiResponse; 75 | let gender: Gender; 76 | 77 | if (req.params.gender === "unknown") { 78 | gender = Gender.UNKNOWN; 79 | } else if (req.params.gender === "male") { 80 | gender = Gender.MALE; 81 | } else if (req.params.gender === "female") { 82 | gender = Gender.FEMALE; 83 | } else { 84 | res.json({ 85 | code: 400, 86 | error: "unrecognized gender argument, shall be either 'male', 'female' or 'unknown'", 87 | } satisfies UsersResponse); 88 | return; 89 | } 90 | 91 | const [err, result] = await _try(services.UserService.getUsers({ gender })); 92 | 93 | if (err instanceof Error) { 94 | res.json({ code: 500, error: err.message } satisfies UsersResponse); 95 | } else { 96 | res.json({ code: 0, data: result.users } satisfies UsersResponse); 97 | } 98 | }).get("/users/age/:min/to/:max", async (req, res) => { 99 | type UsersResponse = ApiResponse; 100 | const minAge = parseInt(req.params.min); 101 | const maxAge = parseInt(req.params.max); 102 | 103 | if (isNaN(minAge) || isNaN(maxAge)) { 104 | res.json({ code: 400, error: "unrecognized age range", } satisfies UsersResponse); 105 | return; 106 | } 107 | 108 | const [err, result] = await _try(services.UserService.getUsers({ minAge, maxAge })); 109 | 110 | if (err instanceof Error) { 111 | res.json({ code: 500, error: err.message } satisfies UsersResponse); 112 | } else { 113 | res.json({ code: 0, data: result.users } satisfies UsersResponse); 114 | } 115 | }).get("/user/:id/posts", async (req, res) => { 116 | type PostsResponse = ApiResponse; 117 | const userId = req.params.id; 118 | const [err, result] = await _try(services.UserService.getMyPosts({ id: userId })); 119 | 120 | if (err instanceof Error) { 121 | res.json({ code: 500, error: err.message } satisfies PostsResponse); 122 | } else { 123 | res.json({ code: 0, data: result.posts } satisfies PostsResponse); 124 | } 125 | }).get("/post/:id", async (req, res) => { 126 | type PostResponse = ApiResponse; 127 | const id = parseInt(req.params.id); 128 | 129 | if (isNaN(id)) { 130 | res.json({ code: 400, error: "invalid post id" } satisfies PostResponse); 131 | return; 132 | } 133 | 134 | const [err, post] = await _try(services.PostService.getPost({ id })); 135 | 136 | if (err instanceof Error) { 137 | if (err.message.includes("not found")) { 138 | res.json({ code: 404, error: err.message } satisfies PostResponse); 139 | } else { 140 | res.json({ code: 500, error: err.message } satisfies PostResponse); 141 | } 142 | } else { 143 | res.json({ code: 0, data: post } satisfies PostResponse); 144 | } 145 | }).get("/posts/search/:keyword", async (req, res) => { 146 | type PostsResponse = ApiResponse; 147 | const [err, result] = await _try(services.PostService.searchPosts({ 148 | keyword: req.params.keyword, 149 | })); 150 | 151 | if (err instanceof Error) { 152 | res.json({ code: 500, error: err.message } satisfies PostsResponse); 153 | } else { 154 | res.json({ code: 0, data: result.posts } satisfies PostsResponse); 155 | } 156 | }); 157 | })(); 158 | --------------------------------------------------------------------------------