Skip to content

Commit a0f001a

Browse files
committed
Added generic RPC client (replaced example)
1 parent b765576 commit a0f001a

File tree

2 files changed

+217
-72
lines changed

2 files changed

+217
-72
lines changed

cmd/arduino-router-client/main.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// This file is part of arduino-router
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-router
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package main
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"net"
22+
"os"
23+
"strconv"
24+
"strings"
25+
26+
"github.com/arduino/arduino-router/msgpackrpc"
27+
"github.com/spf13/cobra"
28+
"gopkg.in/yaml.v3"
29+
)
30+
31+
func main() {
32+
var notification bool
33+
var server string
34+
appname := os.Args[0]
35+
cmd := cobra.Command{
36+
Short: "Send a MsgPack RPC REQUEST or NOTIFICATION.",
37+
Use: appname + " [flags] <METHOD> [<ARG> [<ARG> ...]]\n\n" +
38+
"Send REQUEST: " + appname + " [-s server_addr] <METHOD> [<ARG> [<ARG> ...]]\n" +
39+
"Send NOTIFICATION: " + appname + " [-s server_addr] -n <METHOD> [<ARG> [<ARG> ...]]\n\n" +
40+
" <METHOD> is the method name to request/notify,\n" +
41+
" <ARG> are the arguments to pass to the method:\n" +
42+
" - Use 'true', 'false' for boolean\n" +
43+
" - Use 'null' for null.\n" +
44+
" - Use integer values directly (e.g., 42).\n" +
45+
" - Use the 'f32:' or 'f64:' prefix for floating point values (e.g. f32:3.14159).\n" +
46+
" - Use '[' and ']' to start and end an array.\n" +
47+
" - Use '{' and '}' to start and end a map.\n" +
48+
" Keys and values should be listed in order { KEY1 VAL1 KEY2 VAL2 }.\n" +
49+
" - Any other value is treated as a string, or you may use the 'str:' prefix\n" +
50+
" explicitly in case of ambiguity (e.g. str:42)",
51+
Run: func(cmd *cobra.Command, cliArgs []string) {
52+
// Compose method and arguments
53+
args, rest, err := composeArgs(append(append([]string{"["}, cliArgs[1:]...), "]"))
54+
if err != nil {
55+
fmt.Println("Invalid arguments:", err)
56+
os.Exit(1)
57+
}
58+
if len(rest) > 0 {
59+
fmt.Println("Invalid arguments: extra data:", rest)
60+
os.Exit(1)
61+
}
62+
if _, ok := args.([]any); !ok {
63+
fmt.Println("Invalid arguments: expected array")
64+
os.Exit(1)
65+
}
66+
67+
yamlEncoder := yaml.NewEncoder(os.Stdout)
68+
yamlEncoder.SetIndent(2)
69+
fmt.Println("Sending parameters:")
70+
yamlEncoder.Encode(args)
71+
72+
// Perfom request send
73+
if rpcResp, rpcErr, err := send(server, cliArgs[0], args.([]any), notification); err != nil {
74+
fmt.Println("Error sending request:", err)
75+
os.Exit(1)
76+
} else {
77+
if !notification {
78+
yamlEncoder := yaml.NewEncoder(os.Stdout)
79+
yamlEncoder.SetIndent(2)
80+
if rpcErr != nil {
81+
fmt.Println("Got RPC error response:")
82+
yamlEncoder.Encode(rpcErr)
83+
}
84+
if rpcResp != nil {
85+
fmt.Println("Got RPC response:")
86+
yamlEncoder.Encode(rpcResp)
87+
}
88+
}
89+
}
90+
},
91+
Args: cobra.MinimumNArgs(1),
92+
SilenceUsage: true,
93+
}
94+
cmd.Flags().BoolVarP(
95+
&notification, "notification", "n", false,
96+
"Send a NOTIFICATION instead of a CALL")
97+
cmd.Flags().StringVarP(
98+
&server, "server", "s", "/var/run/arduino-router.sock",
99+
"Server address (file path for unix socket)")
100+
if err := cmd.Execute(); err != nil {
101+
fmt.Printf("Use: %s -h for help.\n", appname)
102+
os.Exit(1)
103+
}
104+
}
105+
106+
func composeArgs(args []string) (any, []string, error) {
107+
if len(args) == 0 {
108+
return nil, nil, fmt.Errorf("no arguments provided")
109+
}
110+
if args[0] == "null" {
111+
return nil, args[1:], nil
112+
}
113+
if args[0] == "true" {
114+
return true, args[1:], nil
115+
}
116+
if args[0] == "false" {
117+
return false, args[1:], nil
118+
}
119+
if f32, ok := strings.CutPrefix(args[0], "f32:"); ok {
120+
f, err := strconv.ParseFloat(f32, 32)
121+
if err != nil {
122+
return nil, args, fmt.Errorf("invalid f32 value: %s", args[0])
123+
}
124+
return float32(f), args[1:], nil
125+
}
126+
if f64, ok := strings.CutPrefix(args[0], "f64:"); ok {
127+
f, err := strconv.ParseFloat(f64, 64)
128+
if err != nil {
129+
return nil, args, fmt.Errorf("invalid f64 value: %s", args[0])
130+
}
131+
return f, args[1:], nil
132+
}
133+
if str, ok := strings.CutPrefix(args[0], "str:"); ok {
134+
return str, args[1:], nil
135+
}
136+
if args[0] == "[" {
137+
arr := []any{}
138+
rest := args[1:]
139+
for {
140+
if len(rest) == 0 {
141+
return nil, args, fmt.Errorf("unterminated array")
142+
}
143+
if rest[0] == "]" {
144+
break
145+
}
146+
if elem, r, err := composeArgs(rest); err != nil {
147+
return nil, args, err
148+
} else {
149+
arr = append(arr, elem)
150+
rest = r
151+
}
152+
}
153+
return arr, rest[1:], nil
154+
}
155+
if args[0] == "{" {
156+
m := make(map[any]any)
157+
rest := args[1:]
158+
for {
159+
if len(rest) == 0 {
160+
return nil, args, fmt.Errorf("unterminated map")
161+
}
162+
if rest[0] == "}" {
163+
break
164+
}
165+
key, r, err := composeArgs(rest)
166+
if err != nil {
167+
return nil, args, fmt.Errorf("invalid map key: %w", err)
168+
}
169+
rest = r
170+
if len(rest) == 0 {
171+
return nil, args, fmt.Errorf("unterminated map (missing value)")
172+
}
173+
value, r, err := composeArgs(rest)
174+
if err != nil {
175+
return nil, args, fmt.Errorf("invalid map value: %w", err)
176+
}
177+
m[key] = value
178+
rest = r
179+
}
180+
return m, rest[1:], nil
181+
}
182+
// Autodetect int or string
183+
if i, err := strconv.Atoi(args[0]); err == nil {
184+
return i, args[1:], nil
185+
}
186+
return args[0], args[1:], nil
187+
}
188+
189+
func send(server string, method string, args []any, notification bool) (any, any, error) {
190+
netType := "unix"
191+
if strings.Contains(server, ":") {
192+
netType = "tcp"
193+
}
194+
c, err := net.Dial(netType, server)
195+
if err != nil {
196+
return nil, nil, fmt.Errorf("error connecting to server: %w", err)
197+
}
198+
199+
conn := msgpackrpc.NewConnection(c, c, nil, nil, nil)
200+
defer conn.Close()
201+
go conn.Run()
202+
203+
// Send notification...
204+
if notification {
205+
if err := conn.SendNotification(method, args); err != nil {
206+
return nil, nil, fmt.Errorf("error sending notification: %w", err)
207+
}
208+
return nil, nil, nil
209+
}
210+
211+
// ...or send Request
212+
reqResult, reqError, err := conn.SendRequest(context.Background(), method, args)
213+
if err != nil {
214+
return nil, nil, fmt.Errorf("error sending request: %w", err)
215+
}
216+
return reqResult, reqError, nil
217+
}

examples/generic_sock_client/main.go

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)