Skip to content

Commit 12aeda4

Browse files
committed
Add support for upload user fields
1 parent 8fb4b95 commit 12aeda4

File tree

8 files changed

+676
-96
lines changed

8 files changed

+676
-96
lines changed

cli/arguments/user_fields.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 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-cli.
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 arguments
17+
18+
import (
19+
"bufio"
20+
"fmt"
21+
"os"
22+
23+
"github.com/arduino/arduino-cli/cli/feedback"
24+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
25+
"golang.org/x/crypto/ssh/terminal"
26+
)
27+
28+
// AskForUserFields prompts the user to input the provided user fields.
29+
// If there is an error reading input it panics.
30+
func AskForUserFields(userFields []*rpc.UserField) map[string]string {
31+
writer := feedback.OutputWriter()
32+
fields := map[string]string{}
33+
reader := bufio.NewReader(os.Stdin)
34+
for _, f := range userFields {
35+
fmt.Fprintf(writer, "%s: ", f.Label)
36+
var value []byte
37+
var err error
38+
if f.Secret {
39+
value, err = terminal.ReadPassword(int(os.Stdin.Fd()))
40+
} else {
41+
value, err = reader.ReadBytes('\n')
42+
}
43+
if err != nil {
44+
panic(err)
45+
}
46+
fields[f.Name] = string(value)
47+
}
48+
fmt.Fprintln(writer, "")
49+
50+
return fields
51+
}

cli/compile/compile.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,33 @@ func run(cmd *cobra.Command, args []string) {
187187
}
188188

189189
if err == nil && uploadAfterCompile {
190-
191190
var sk *sketch.Sketch
192191
sk, err = sketch.New(sketchPath)
193192
if err != nil {
194-
193+
feedback.Errorf("Error during Upload: %v", err)
194+
os.Exit(errorcodes.ErrGeneric)
195195
}
196196
var discoveryPort *discovery.Port
197197
discoveryPort, err = port.GetPort(inst, sk)
198198
if err != nil {
199+
feedback.Errorf("Error during Upload: %v", err)
200+
os.Exit(errorcodes.ErrGeneric)
201+
}
202+
203+
userFieldRes, err := upload.SupportedUserFields(context.Background(), &rpc.SupportedUserFieldsRequest{
204+
Instance: inst,
205+
Fqbn: fqbn,
206+
Protocol: discoveryPort.Protocol,
207+
})
208+
if err != nil {
209+
feedback.Errorf("Error during Upload: %v", err)
210+
os.Exit(errorcodes.ErrGeneric)
211+
}
199212

213+
fields := map[string]string{}
214+
if len(userFieldRes.UserFields) > 0 {
215+
feedback.Printf("Uploading to specified board using %s protocol requires the following info:", discoveryPort.Protocol)
216+
fields = arguments.AskForUserFields(userFieldRes.UserFields)
200217
}
201218

202219
uploadRequest := &rpc.UploadRequest{
@@ -208,8 +225,8 @@ func run(cmd *cobra.Command, args []string) {
208225
Verify: verify,
209226
ImportDir: buildPath,
210227
Programmer: programmer,
228+
UserFields: fields,
211229
}
212-
var err error
213230
if output.OutputFormat == "json" {
214231
// TODO: do not print upload output in json mode
215232
uploadOut := new(bytes.Buffer)

cli/upload/upload.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ func run(command *cobra.Command, args []string) {
9999
os.Exit(errorcodes.ErrGeneric)
100100
}
101101

102+
userFieldRes, err := upload.SupportedUserFields(context.Background(), &rpc.SupportedUserFieldsRequest{
103+
Instance: instance,
104+
Fqbn: fqbn,
105+
Protocol: discoveryPort.Protocol,
106+
})
107+
if err != nil {
108+
feedback.Errorf("Error during Upload: %v", err)
109+
os.Exit(errorcodes.ErrGeneric)
110+
}
111+
112+
fields := map[string]string{}
113+
if len(userFieldRes.UserFields) > 0 {
114+
feedback.Printf("Uploading to specified board using %s protocol requires the following info:", discoveryPort.Protocol)
115+
fields = arguments.AskForUserFields(userFieldRes.UserFields)
116+
}
117+
102118
if _, err := upload.Upload(context.Background(), &rpc.UploadRequest{
103119
Instance: instance,
104120
Fqbn: fqbn,
@@ -110,6 +126,7 @@ func run(command *cobra.Command, args []string) {
110126
ImportDir: importDir,
111127
Programmer: programmer,
112128
DryRun: dryRun,
129+
UserFields: fields,
113130
}, os.Stdout, os.Stderr); err != nil {
114131
feedback.Errorf("Error during Upload: %v", err)
115132
os.Exit(errorcodes.ErrGeneric)

commands/upload/burnbootloader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func BurnBootloader(ctx context.Context, req *rpc.BurnBootloaderRequest, outStre
4848
outStream,
4949
errStream,
5050
req.GetDryRun(),
51+
map[string]string{}, // User fields
5152
)
5253
if err != nil {
5354
return nil, err

commands/upload/upload.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"path/filepath"
23+
"strconv"
2324
"strings"
2425

2526
"github.com/arduino/arduino-cli/arduino/cores"
@@ -36,6 +37,43 @@ import (
3637
"github.com/sirupsen/logrus"
3738
)
3839

40+
// SupportedUserFields returns a SupportedUserFieldsResponse containing all the UserFields supported
41+
// by the upload tools needed by the board using the protocol specified in SupportedUserFieldsRequest.
42+
func SupportedUserFields(ctx context.Context, req *rpc.SupportedUserFieldsRequest) (*rpc.SupportedUserFieldsResponse, error) {
43+
if req.Protocol == "" {
44+
return nil, fmt.Errorf("missing protocol")
45+
}
46+
47+
pm := commands.GetPackageManager(req.GetInstance().GetId())
48+
if pm == nil {
49+
return nil, fmt.Errorf("invalid instance")
50+
}
51+
52+
fqbn, err := cores.ParseFQBN(req.GetFqbn())
53+
if err != nil {
54+
return nil, fmt.Errorf("parsing fqbn: %s", err)
55+
}
56+
57+
_, platformRelease, board, _, _, err := pm.ResolveFQBN(fqbn)
58+
if err != nil {
59+
return nil, fmt.Errorf("loading board data: %s", err)
60+
}
61+
62+
toolId, err := getToolId(board.Properties, "upload", req.Protocol)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
userFields, err := getUserFields(toolId, platformRelease)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
return &rpc.SupportedUserFieldsResponse{
73+
UserFields: userFields,
74+
}, nil
75+
}
76+
3977
// getToolId returns the ID of the tool that supports the action and protocol combination by searching in props.
4078
// Returns error if tool cannot be found.
4179
func getToolId(props *properties.Map, action, protocol string) (string, error) {
@@ -52,6 +90,35 @@ func getToolId(props *properties.Map, action, protocol string) (string, error) {
5290
return "", fmt.Errorf("cannot find tool: undefined '%s' property", toolProperty)
5391
}
5492

93+
// getUserFields return all user fields supported by the tools specified.
94+
// Returns error only in case the secret property is not a valid boolean.
95+
func getUserFields(toolId string, platformRelease *cores.PlatformRelease) ([]*rpc.UserField, error) {
96+
userFields := []*rpc.UserField{}
97+
fields := platformRelease.Properties.SubTree(fmt.Sprintf("tools.%s.upload.field", toolId))
98+
keys := fields.FirstLevelKeys()
99+
100+
for _, key := range keys {
101+
value := fields.Get(key)
102+
secretProp := fmt.Sprintf("%s.secret", key)
103+
secret, ok := fields.GetOk(secretProp)
104+
if !ok {
105+
secret = "false"
106+
}
107+
isSecret, err := strconv.ParseBool(secret)
108+
if err != nil {
109+
return nil, fmt.Errorf(`parsing "tools.%s.upload.field.%s.secret", property is not a boolean`, toolId, key)
110+
}
111+
userFields = append(userFields, &rpc.UserField{
112+
ToolId: toolId,
113+
Name: key,
114+
Label: value,
115+
Secret: isSecret,
116+
})
117+
}
118+
119+
return userFields, nil
120+
}
121+
55122
// Upload FIXMEDOC
56123
func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, errStream io.Writer) (*rpc.UploadResponse, error) {
57124
logrus.Tracef("Upload %s on %s started", req.GetSketchPath(), req.GetFqbn())
@@ -80,6 +147,7 @@ func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, er
80147
outStream,
81148
errStream,
82149
req.GetDryRun(),
150+
req.GetUserFields(),
83151
)
84152
if err != nil {
85153
return nil, err
@@ -104,6 +172,7 @@ func UsingProgrammer(ctx context.Context, req *rpc.UploadUsingProgrammerRequest,
104172
Programmer: req.GetProgrammer(),
105173
Verbose: req.GetVerbose(),
106174
Verify: req.GetVerify(),
175+
UserFields: req.GetUserFields(),
107176
}, outStream, errStream)
108177
return &rpc.UploadUsingProgrammerResponse{}, err
109178
}
@@ -114,7 +183,7 @@ func runProgramAction(pm *packagemanager.PackageManager,
114183
programmerID string,
115184
verbose, verify, burnBootloader bool,
116185
outStream, errStream io.Writer,
117-
dryRun bool) error {
186+
dryRun bool, userFields map[string]string) error {
118187

119188
if burnBootloader && programmerID == "" {
120189
return fmt.Errorf("no programmer specified for burning bootloader")
@@ -228,6 +297,14 @@ func runProgramAction(pm *packagemanager.PackageManager,
228297
}
229298
}
230299

300+
// Certain tools require the user to provide custom fields at run time,
301+
// if they've been provided set them
302+
// For more info:
303+
// https://github.com/arduino/tooling-rfcs/blob/main/RFCs/0002-pluggable-discovery.md#user-provided-fields
304+
for name, value := range userFields {
305+
uploadProperties.Set(fmt.Sprintf("%s.field.%s", action, name), value)
306+
}
307+
231308
if !uploadProperties.ContainsKey("upload.protocol") && programmer == nil {
232309
return fmt.Errorf("a programmer is required to upload for this board")
233310
}
@@ -364,6 +441,11 @@ func runProgramAction(pm *packagemanager.PackageManager,
364441
}
365442
}
366443

444+
// Get Port properties gathered using Pluggable Discovery
445+
for prop, value := range actualPort.Properties {
446+
uploadProperties.Set(fmt.Sprintf("upload.port.properties.%s", prop), value)
447+
}
448+
367449
// Run recipes for upload
368450
if burnBootloader {
369451
if err := runTool("erase.pattern", uploadProperties, outStream, errStream, verbose, dryRun); err != nil {

commands/upload/upload_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,40 @@ upload.tool.network=arduino_ota`))
266266
require.NoError(t, err)
267267
require.Equal(t, "avrdude", toolId)
268268
}
269+
270+
func TestGetUserFields(t *testing.T) {
271+
platformRelease := &cores.PlatformRelease{}
272+
273+
props, err := properties.LoadFromBytes([]byte(`
274+
tools.avrdude.upload.field.username=Username
275+
tools.avrdude.upload.field.password=Password
276+
tools.avrdude.upload.field.password.secret=true
277+
tools.arduino_ota.upload.field.username=Username
278+
tools.arduino_ota.upload.field.password=Password
279+
tools.arduino_ota.upload.field.password.secret=true`))
280+
require.NoError(t, err)
281+
282+
platformRelease.Properties = props
283+
284+
userFields, err := getUserFields("avrdude", platformRelease)
285+
require.NoError(t, err)
286+
require.Len(t, userFields, 2)
287+
require.Equal(t, userFields[0].ToolId, "avrdude")
288+
require.Equal(t, userFields[0].Name, "username")
289+
require.Equal(t, userFields[0].Label, "Username")
290+
require.False(t, userFields[0].Secret)
291+
require.Equal(t, userFields[1].ToolId, "avrdude")
292+
require.Equal(t, userFields[1].Name, "password")
293+
require.Equal(t, userFields[1].Label, "Password")
294+
require.True(t, userFields[1].Secret)
295+
296+
props, err = properties.LoadFromBytes([]byte(`
297+
tools.arduino_ota.upload.field.password=Password
298+
tools.arduino_ota.upload.field.password.secret=THIS_IS_NOT_A_BOOLEAN`))
299+
require.NoError(t, err)
300+
platformRelease.Properties = props
301+
302+
userFields, err = getUserFields("arduino_ota", platformRelease)
303+
require.Nil(t, userFields)
304+
require.EqualError(t, err, `parsing "tools.arduino_ota.upload.field.password.secret", property is not a boolean`)
305+
}

0 commit comments

Comments
 (0)