Skip to content

Commit ec027a7

Browse files
silvanocerzacmaglieper1234
authored
Add support for Pluggable Discoveries (#1333)
* [skip changelog] Add DiscoveryManager to PackageManager * Add loading of PluggableDiscoveries when loading a platform release * Added compatibility layer for non-pluggable platforms * Implemented board list with discoveries * Implemented discovery loading after initialization * Implemented board watch with discoveries * Fix load discoveries tests * Fix some issues with board list watcher * Fix FindToolsRequiredFromPlatformRelease not returning discoveries * Enhanced handling of some discoveries states * Fix PackageManager reset * Add function to convert discovery.Port to rpc.Port * Moved reference argument parsing to new package * Fix functions docstrings * Remove duplicated code to initialize Sketch path * Add property conversion for platform not supporting pluggable discovery * Fix board list watch not working * Fix crash when converting Port to rpc struct * Add generic Port argument * Change gRPC upload functions to use new Port message * Add support for upload user fields * Fix upload unit tests * Fix code naming issues * Added builtin:mdns-discovery * Do not panic if discovery tool is not installed * Implemented port/protocol detection at CLI startup time * Perform 1200bps-touch only on serial ports * Added missing properties for pluggable upload * Correctly implemented 'board list' timeout option * Updated mdns-discovery to 0.9.2 * Add documentation * Add board properties to board list command and gRPC function * Fix documentation and code comments Co-authored-by: per1234 <[email protected]> * Fix crash when attempting upload without specifying port address * Fix unit tests * Update go-properties-orderedmap to fix discovery properties issues * Fix more documentation Co-authored-by: per1234 <[email protected]> * Clarify pluggable discovery specification * More documentation fixes * Add upload_port properties docs in platform specification * Change links from pluggable discovery RFC to official docs * Add more upload mock integration tests * Fix integration tests * Change property to declare pluggable discoveries * Change property to declare pluggable discoveries * Fix documentation Co-authored-by: per1234 <[email protected]> * Fix loading of platform not supporting pluggable discovery * Fix more documentation Co-authored-by: per1234 <[email protected]> * Add pluggable discovery states documentation * Enhanced handling of pluggable discoveries states * Discoveries processes are now killed if the HELLO command fails * Add pluggable discovery logging * Enhanced handling of failing pluggable discoveries * Fix pluggable discoveries parallelization * Discoveries event channels are now created when start sync is called * Cached ports are now reset on discovery stop * Renamed ListSync methods to ListCachedPorts * Pluggable discovery upload user fields are now limited to 50 chars * Fix i18n strings * Fix failing integration tests * Fix i18n data * Fix integration tests again * [skip changelog] Internationalize strings added for pluggable discovery support (#1384) * Update docs/pluggable-discovery-specification.md Co-authored-by: per1234 <[email protected]> * Fix failing workflows * Updated upload-mock tests for generation * Added a lot of mock upload test (also with programmer option) * test_upload_mock: Handle '{' and '}' in recipes * network ota: autoconvert network_patter from legacy * Automatically add port detection properties for network discovery * Slightly improved 'board list' text output * Default 'board list' timeout to 1s * Added some code review fixes * Added unit test for legacy-package conversion to pluggable discovery Co-authored-by: Cristian Maglie <[email protected]> Co-authored-by: per1234 <[email protected]>
1 parent 3aceff5 commit ec027a7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+13916
-6213
lines changed

Diff for: .flake8

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
[flake8]
77
doctests = True
8+
per-file-ignores =
9+
test/test_upload_mock.py:E501
810
ignore =
911
E741,
1012
# W503 and W504 are mutually exclusive. PEP 8 recommends line break before.

Diff for: arduino/cores/packagemanager/loader.go

+191-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"strings"
2424

2525
"github.com/arduino/arduino-cli/arduino/cores"
26+
"github.com/arduino/arduino-cli/arduino/discovery"
2627
"github.com/arduino/arduino-cli/configuration"
2728
"github.com/arduino/go-paths-helper"
2829
properties "github.com/arduino/go-properties-orderedmap"
@@ -320,8 +321,11 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
320321
return fmt.Errorf(tr("loading %[1]s: %[2]s"), platformTxtLocalPath, err)
321322
}
322323

323-
if platform.Properties.SubTree("discovery").Size() > 0 {
324+
if platform.Properties.SubTree("pluggable_discovery").Size() > 0 {
324325
platform.PluggableDiscoveryAware = true
326+
} else {
327+
platform.Properties.Set("pluggable_discovery.required.0", "builtin:serial-discovery")
328+
platform.Properties.Set("pluggable_discovery.required.1", "builtin:mdns-discovery")
325329
}
326330

327331
if platform.Platform.Name == "" {
@@ -337,8 +341,11 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
337341

338342
// Create programmers properties
339343
if programmersProperties, err := properties.SafeLoad(programmersTxtPath.String()); err == nil {
340-
for programmerID, programmerProperties := range programmersProperties.FirstLevelOf() {
341-
platform.Programmers[programmerID] = pm.loadProgrammer(programmerProperties)
344+
for programmerID, programmerProps := range programmersProperties.FirstLevelOf() {
345+
if !platform.PluggableDiscoveryAware {
346+
convertUploadToolsToPluggableDiscovery(programmerProps)
347+
}
348+
platform.Programmers[programmerID] = pm.loadProgrammer(programmerProps)
342349
platform.Programmers[programmerID].PlatformRelease = platform
343350
}
344351
} else {
@@ -349,9 +356,71 @@ func (pm *PackageManager) loadPlatformRelease(platform *cores.PlatformRelease, p
349356
return fmt.Errorf(tr("loading boards: %s"), err)
350357
}
351358

359+
if !platform.PluggableDiscoveryAware {
360+
convertLegacyPlatformToPluggableDiscovery(platform)
361+
}
352362
return nil
353363
}
354364

365+
func convertLegacyPlatformToPluggableDiscovery(platform *cores.PlatformRelease) {
366+
toolsProps := platform.Properties.SubTree("tools").FirstLevelOf()
367+
for toolName, toolProps := range toolsProps {
368+
if !toolProps.ContainsKey("upload.network_pattern") {
369+
continue
370+
}
371+
372+
// Convert network_pattern configuration to pluggable discovery
373+
convertedToolName := toolName + "__pluggable_network"
374+
convertedProps := convertLegacyNetworkPatternToPluggableDiscovery(toolProps, convertedToolName)
375+
376+
// Merge the converted properties in the root configuration
377+
platform.Properties.Merge(convertedProps)
378+
379+
// Add the network upload to the boards using the old method
380+
for _, board := range platform.Boards {
381+
oldUploadTool := board.Properties.Get("upload.tool")
382+
if oldUploadTool == toolName && !board.Properties.ContainsKey("upload.tool.network") {
383+
board.Properties.Set("upload.tool.network", convertedToolName)
384+
385+
// Add identification properties for network protocol
386+
i := 0
387+
for {
388+
if !board.Properties.ContainsKey(fmt.Sprintf("upload_port.%d.vid", i)) {
389+
break
390+
}
391+
i++
392+
}
393+
board.Properties.Set(fmt.Sprintf("upload_port.%d.board", i), board.BoardID)
394+
}
395+
}
396+
}
397+
}
398+
399+
func convertLegacyNetworkPatternToPluggableDiscovery(props *properties.Map, newToolName string) *properties.Map {
400+
pattern, ok := props.GetOk("upload.network_pattern")
401+
if !ok {
402+
return nil
403+
}
404+
props.Remove("upload.network_pattern")
405+
pattern = strings.ReplaceAll(pattern, "{serial.port}", "{upload.port.address}")
406+
pattern = strings.ReplaceAll(pattern, "{network.port}", "{upload.port.properties.port}")
407+
if strings.Contains(pattern, "{network.password}") {
408+
props.Set("upload.field.password", "Password")
409+
props.Set("upload.field.password.secret", "true")
410+
pattern = strings.ReplaceAll(pattern, "{network.password}", "{upload.field.password}")
411+
}
412+
props.Set("upload.pattern", pattern)
413+
414+
prefix := "tools." + newToolName + "."
415+
res := properties.NewMap()
416+
for _, k := range props.Keys() {
417+
v := props.Get(k)
418+
res.Set(prefix+k, v)
419+
// fmt.Println("ADDED:", prefix+k+"="+v)
420+
}
421+
return res
422+
}
423+
355424
func (pm *PackageManager) loadProgrammer(programmerProperties *properties.Map) *cores.Programmer {
356425
return &cores.Programmer{
357426
Name: programmerProperties.Get("name"),
@@ -388,12 +457,6 @@ func (pm *PackageManager) loadBoards(platform *cores.PlatformRelease) error {
388457
// set all other boards properties
389458
delete(propertiesByBoard, "menu")
390459

391-
if !platform.PluggableDiscoveryAware {
392-
for _, boardProperties := range propertiesByBoard {
393-
convertVidPidIdentificationPropertiesToPluggableDiscovery(boardProperties)
394-
}
395-
}
396-
397460
skippedBoards := []string{}
398461
for boardID, boardProperties := range propertiesByBoard {
399462
var board *cores.Board
@@ -412,6 +475,12 @@ func (pm *PackageManager) loadBoards(platform *cores.PlatformRelease) error {
412475
goto next_board
413476
}
414477
}
478+
479+
if !platform.PluggableDiscoveryAware {
480+
convertVidPidIdentificationPropertiesToPluggableDiscovery(boardProperties)
481+
convertUploadToolsToPluggableDiscovery(boardProperties)
482+
}
483+
415484
// The board's ID must be available in a board's properties since it can
416485
// be used in all configuration files for several reasons, like setting compilation
417486
// flags depending on the board id.
@@ -468,6 +537,22 @@ func convertVidPidIdentificationPropertiesToPluggableDiscovery(boardProperties *
468537
}
469538
}
470539

540+
func convertUploadToolsToPluggableDiscovery(props *properties.Map) {
541+
actions := []string{"upload", "bootloader", "program"}
542+
for _, action := range actions {
543+
if !props.ContainsKey(fmt.Sprintf("%s.tool.default", action)) {
544+
tool, found := props.GetOk(fmt.Sprintf("%s.tool", action))
545+
if !found {
546+
// Just skip it, ideally this must never happen but if a platform
547+
// doesn't define an expected upload.tool, bootloader.tool or program.tool
548+
// there will be other issues further down the road after this conversion
549+
continue
550+
}
551+
props.Set(fmt.Sprintf("%s.tool.default", action), tool)
552+
}
553+
}
554+
}
555+
471556
func (pm *PackageManager) loadToolsFromPackage(targetPackage *cores.Package, toolsPath *paths.Path) []*status.Status {
472557
pm.Log.Infof("Loading tools from dir: %s", toolsPath)
473558

@@ -587,3 +672,100 @@ func (pm *PackageManager) LoadToolsFromBundleDirectory(toolsPath *paths.Path) er
587672
}
588673
return nil
589674
}
675+
676+
// LoadDiscoveries load all discoveries for all loaded platforms
677+
// Returns error if:
678+
// * A PluggableDiscovery instance can't be created
679+
// * Tools required by the PlatformRelease cannot be found
680+
// * Command line to start PluggableDiscovery has malformed or mismatched quotes
681+
func (pm *PackageManager) LoadDiscoveries() []*status.Status {
682+
statuses := []*status.Status{}
683+
for _, platform := range pm.InstalledPlatformReleases() {
684+
statuses = append(statuses, pm.loadDiscoveries(platform)...)
685+
}
686+
return statuses
687+
}
688+
689+
func (pm *PackageManager) loadDiscoveries(release *cores.PlatformRelease) []*status.Status {
690+
statuses := []*status.Status{}
691+
discoveryProperties := release.Properties.SubTree("pluggable_discovery")
692+
693+
if discoveryProperties.Size() == 0 {
694+
return nil
695+
}
696+
697+
// Handles discovery properties formatted like so:
698+
//
699+
// Case 1:
700+
// "pluggable_discovery.required": "PLATFORM:DISCOVERY_NAME",
701+
//
702+
// Case 2:
703+
// "pluggable_discovery.required.0": "PLATFORM:DISCOVERY_ID_1",
704+
// "pluggable_discovery.required.1": "PLATFORM:DISCOVERY_ID_2",
705+
//
706+
// If both indexed and unindexed properties are found the unindexed are ignored
707+
for _, id := range discoveryProperties.ExtractSubIndexLists("required") {
708+
tool := pm.GetTool(id)
709+
if tool == nil {
710+
statuses = append(statuses, status.Newf(codes.FailedPrecondition, tr("discovery not found: %s"), id))
711+
continue
712+
}
713+
toolRelease := tool.GetLatestInstalled()
714+
if toolRelease == nil {
715+
statuses = append(statuses, status.Newf(codes.FailedPrecondition, tr("discovery not installed: %s"), id))
716+
continue
717+
}
718+
discoveryPath := toolRelease.InstallDir.Join(tool.Name).String()
719+
d, err := discovery.New(id, discoveryPath)
720+
if err != nil {
721+
statuses = append(statuses, status.Newf(codes.FailedPrecondition, tr("creating discovery: %s"), err))
722+
continue
723+
}
724+
pm.discoveryManager.Add(d)
725+
}
726+
727+
discoveryIDs := discoveryProperties.FirstLevelOf()
728+
delete(discoveryIDs, "required")
729+
// Get the list of tools only if there are discoveries that use Direct discovery integration.
730+
// See:
731+
// https://arduino.github.io/arduino-cli/latest/platform-specification/#pluggable-discovery
732+
// We need the tools only in that case since we might need some tool's
733+
// runtime properties to expand the discovery pattern to run it correctly.
734+
var tools []*cores.ToolRelease
735+
if len(discoveryIDs) > 0 {
736+
var err error
737+
tools, err = pm.FindToolsRequiredFromPlatformRelease(release)
738+
if err != nil {
739+
statuses = append(statuses, status.New(codes.Internal, err.Error()))
740+
}
741+
}
742+
743+
// Handles discovery properties formatted like so:
744+
//
745+
// discovery.DISCOVERY_ID.pattern: "COMMAND_TO_EXECUTE"
746+
for discoveryID, props := range discoveryIDs {
747+
pattern, ok := props.GetOk("pattern")
748+
if !ok {
749+
statuses = append(statuses, status.Newf(codes.FailedPrecondition, tr("can't find pattern for discovery with id %s"), discoveryID))
750+
continue
751+
}
752+
configuration := release.Properties.Clone()
753+
configuration.Merge(release.RuntimeProperties())
754+
configuration.Merge(props)
755+
756+
for _, tool := range tools {
757+
configuration.Merge(tool.RuntimeProperties())
758+
}
759+
760+
cmd := configuration.ExpandPropsInString(pattern)
761+
if cmdArgs, err := properties.SplitQuotedString(cmd, `"'`, true); err != nil {
762+
statuses = append(statuses, status.New(codes.Internal, err.Error()))
763+
} else if d, err := discovery.New(discoveryID, cmdArgs...); err != nil {
764+
statuses = append(statuses, status.New(codes.Internal, err.Error()))
765+
} else {
766+
pm.discoveryManager.Add(d)
767+
}
768+
}
769+
770+
return statuses
771+
}

0 commit comments

Comments
 (0)