diff --git a/Gopkg.lock b/Gopkg.lock index 8e7a2ccc610..f55ad3cc418 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -432,7 +432,6 @@ "github.com/arduino/go-win32-utils", "github.com/bgentry/go-netrc/netrc", "github.com/bouk/monkey", - "github.com/codeclysm/cc", "github.com/codeclysm/extract", "github.com/fatih/color", "github.com/gosuri/uitable", diff --git a/Gopkg.toml b/Gopkg.toml index 012e9ab1530..42a9f1ff0a6 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -51,10 +51,6 @@ branch = "master" name = "github.com/bgentry/go-netrc" -[[constraint]] - name = "github.com/codeclysm/cc" - version = "1.2.1" - [[constraint]] branch = "master" name = "github.com/mitchellh/go-homedir" diff --git a/arduino/cores/cores.go b/arduino/cores/cores.go index c7037b10681..eb831be452d 100644 --- a/arduino/cores/cores.go +++ b/arduino/cores/cores.go @@ -20,11 +20,11 @@ package cores import ( "strings" - "github.com/arduino/go-paths-helper" + paths "github.com/arduino/go-paths-helper" "github.com/arduino/arduino-cli/arduino/resources" - "github.com/arduino/go-properties-orderedmap" - "go.bug.st/relaxed-semver" + properties "github.com/arduino/go-properties-orderedmap" + semver "go.bug.st/relaxed-semver" ) // Platform represents a platform package. @@ -158,7 +158,7 @@ func (platform *Platform) latestReleaseVersion() *semver.Version { func (platform *Platform) GetAllInstalled() []*PlatformRelease { res := []*PlatformRelease{} for _, release := range platform.Releases { - if release.InstallDir != nil { + if release.IsInstalled() { res = append(res, release) } } diff --git a/arduino/cores/packagemanager/identify.go b/arduino/cores/packagemanager/identify.go new file mode 100644 index 00000000000..834cd95d00e --- /dev/null +++ b/arduino/cores/packagemanager/identify.go @@ -0,0 +1,65 @@ +/* + * This file is part of arduino-cli. + * + * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) + * + * This software is released under the GNU General Public License version 3, + * which covers the main part of arduino-cli. + * The terms of this license can be found at: + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * You can be released from the requirements of the above licenses by purchasing + * a commercial license. Buying such a license is mandatory if you want to modify or + * otherwise use the software for commercial activities involving the Arduino + * software without disclosing the source code of your own applications. To purchase + * a commercial license, send an email to license@arduino.cc. + */ + +package packagemanager + +import ( + "fmt" + + "github.com/arduino/arduino-cli/arduino/cores" + properties "github.com/arduino/go-properties-orderedmap" +) + +// IdentifyBoard returns a list of baords matching the provided identification properties. +func (pm *PackageManager) IdentifyBoard(idProps *properties.Map) []*cores.Board { + if idProps.Size() == 0 { + return []*cores.Board{} + } + + checkSuffix := func(props *properties.Map, s string) (checked bool, found bool) { + for k, v1 := range idProps.AsMap() { + v2, ok := props.GetOk(k + s) + if !ok { + return false, false + } + if v1 != v2 { + return true, false + } + } + return false, true + } + + foundBoards := []*cores.Board{} + for _, board := range pm.InstalledBoards() { + if _, found := checkSuffix(board.Properties, ""); found { + foundBoards = append(foundBoards, board) + continue + } + id := 0 + for { + again, found := checkSuffix(board.Properties, fmt.Sprintf(".%d", id)) + if found { + foundBoards = append(foundBoards, board) + } + if !again { + break + } + id++ + } + } + return foundBoards +} diff --git a/arduino/cores/packagemanager/package_manager.go b/arduino/cores/packagemanager/package_manager.go index a8b455aed6c..3e50c3a3cbf 100644 --- a/arduino/cores/packagemanager/package_manager.go +++ b/arduino/cores/packagemanager/package_manager.go @@ -26,10 +26,10 @@ import ( "github.com/arduino/arduino-cli/arduino/cores" "github.com/arduino/arduino-cli/arduino/cores/packageindex" - "github.com/arduino/go-paths-helper" + paths "github.com/arduino/go-paths-helper" properties "github.com/arduino/go-properties-orderedmap" "github.com/sirupsen/logrus" - "go.bug.st/relaxed-semver" + semver "go.bug.st/relaxed-semver" ) // PackageManager defines the superior oracle which understands all about @@ -354,6 +354,36 @@ func (pm *PackageManager) GetAllInstalledToolsReleases() []*cores.ToolRelease { return tools } +// InstalledPlatformReleases returns all installed PlatformReleases. This function is +// useful to range all PlatformReleases in for loops. +func (pm *PackageManager) InstalledPlatformReleases() []*cores.PlatformRelease { + platforms := []*cores.PlatformRelease{} + for _, targetPackage := range pm.packages.Packages { + for _, platform := range targetPackage.Platforms { + for _, release := range platform.GetAllInstalled() { + platforms = append(platforms, release) + } + } + } + return platforms +} + +// InstalledBoards returns all installed Boards. This function is useful to range +// all Boards in for loops. +func (pm *PackageManager) InstalledBoards() []*cores.Board { + boards := []*cores.Board{} + for _, targetPackage := range pm.packages.Packages { + for _, platform := range targetPackage.Platforms { + for _, release := range platform.GetAllInstalled() { + for _, board := range release.Boards { + boards = append(boards, board) + } + } + } + } + return boards +} + func (pm *PackageManager) FindToolsRequiredForBoard(board *cores.Board) ([]*cores.ToolRelease, error) { pm.Log.Infof("Searching tools required for board %s", board) diff --git a/arduino/discovery/discovery.go b/arduino/discovery/discovery.go new file mode 100644 index 00000000000..46925599e5e --- /dev/null +++ b/arduino/discovery/discovery.go @@ -0,0 +1,152 @@ +// +// This file is part of arduino-cli. +// +// Copyright 2018 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to modify or +// otherwise use the software for commercial activities involving the Arduino +// software without disclosing the source code of your own applications. To purchase +// a commercial license, send an email to license@arduino.cc. +// + +package discovery + +import ( + "encoding/json" + "fmt" + "io" + "os/exec" + "time" + + "github.com/arduino/arduino-cli/arduino/cores/packagemanager" + + properties "github.com/arduino/go-properties-orderedmap" + + "github.com/arduino/arduino-cli/executils" +) + +// Discovery is an instance of a discovery tool +type Discovery struct { + in io.WriteCloser + out io.ReadCloser + outJSON *json.Decoder + cmd *exec.Cmd + Timeout time.Duration +} + +// BoardPort is a generic port descriptor +type BoardPort struct { + Address string `json:"address"` + Label string `json:"label"` + Prefs *properties.Map `json:"prefs"` + IdentificationPrefs *properties.Map `json:"identificationPrefs"` + Protocol string `json:"protocol"` + ProtocolLabel string `json:"protocolLabel"` +} + +type eventJSON struct { + EventType string `json:"eventType,required"` + Ports []*BoardPort `json:"ports"` +} + +// NewFromCommandLine creates a new Discovery object +func NewFromCommandLine(args ...string) (*Discovery, error) { + cmd, err := executils.Command(args) + if err != nil { + return nil, fmt.Errorf("creating discovery process: %s", err) + } + disc := &Discovery{Timeout: time.Second} + disc.cmd = cmd + return disc, nil +} + +// Start starts the specified discovery +func (d *Discovery) Start() error { + if in, err := d.cmd.StdinPipe(); err == nil { + d.in = in + } else { + return fmt.Errorf("creating stdin pipe for discovery: %s", err) + } + if out, err := d.cmd.StdoutPipe(); err == nil { + d.out = out + d.outJSON = json.NewDecoder(d.out) + } else { + return fmt.Errorf("creating stdout pipe for discovery: %s", err) + } + if err := d.cmd.Start(); err != nil { + return fmt.Errorf("starting discovery process: %s", err) + } + return nil +} + +// List retrieve the port list from this discovery +func (d *Discovery) List() ([]*BoardPort, error) { + if _, err := d.in.Write([]byte("LIST\n")); err != nil { + return nil, fmt.Errorf("sending LIST command to discovery: %s", err) + } + var event eventJSON + done := make(chan bool) + timeout := false + go func() { + select { + case <-done: + case <-time.After(d.Timeout): + timeout = true + d.Close() + } + }() + if err := d.outJSON.Decode(&event); err != nil { + if timeout { + return nil, fmt.Errorf("decoding LIST command: timeout") + } + return nil, fmt.Errorf("decoding LIST command: %s", err) + } + done <- true + return event.Ports, nil +} + +// Close stops the Discovery and free the resources +func (d *Discovery) Close() error { + // TODO: Send QUIT for safe close or terminate process after a small timeout + if err := d.in.Close(); err != nil { + return fmt.Errorf("closing stdin pipe: %s", err) + } + if err := d.out.Close(); err != nil { + return fmt.Errorf("closing stdout pipe: %s", err) + } + if d.cmd != nil { + d.cmd.Process.Kill() + } + return nil +} + +// ExtractDiscoveriesFromPlatforms returns all Discovery from all the installed platforms. +func ExtractDiscoveriesFromPlatforms(pm *packagemanager.PackageManager) map[string]*Discovery { + res := map[string]*Discovery{} + + for _, platformRelease := range pm.InstalledPlatformReleases() { + discoveries := platformRelease.Properties.SubTree("discovery").FirstLevelOf() + + for name, props := range discoveries { + if pattern, has := props.GetOk("pattern"); has { + props.Merge(platformRelease.Properties) + cmdLine := props.ExpandPropsInString(pattern) + if cmdArgs, err := properties.SplitQuotedString(cmdLine, `"`, false); err != nil { + // TODO + } else if disc, err := NewFromCommandLine(cmdArgs...); err != nil { + // TODO + } else { + res[name] = disc + } + } + } + } + + return res +} diff --git a/commands/board/list.go b/commands/board/list.go index 8d604153a18..34eef176e65 100644 --- a/commands/board/list.go +++ b/commands/board/list.go @@ -18,15 +18,18 @@ package board import ( + "encoding/json" "fmt" + "os" + "sort" "time" - "github.com/arduino/arduino-cli/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/output" + + "github.com/arduino/arduino-cli/arduino/discovery" "github.com/arduino/arduino-cli/commands" + "github.com/arduino/arduino-cli/commands/core" "github.com/arduino/arduino-cli/common/formatter" - "github.com/arduino/arduino-cli/common/formatter/output" - "github.com/arduino/board-discovery" - "github.com/codeclysm/cc" "github.com/spf13/cobra" ) @@ -35,13 +38,13 @@ func initListCommand() *cobra.Command { Use: "list", Short: "List connected boards.", Long: "Detects and displays a list of connected boards to the current computer.", - Example: " " + commands.AppName + " board list --timeout 10s", + Example: " " + commands.AppName + " board list", Args: cobra.NoArgs, Run: runListCommand, } - listCommand.Flags().StringVar(&listFlags.timeout, "timeout", "5s", - "The timeout of the search of connected devices, try to high it if your board is not found (e.g. to 10s).") + listCommand.Flags().StringVar(&listFlags.timeout, "timeout", "1s", + "The timeout of the search of connected devices, try to increase it if your board is not found (e.g. to 10s).") return listCommand } @@ -50,95 +53,150 @@ var listFlags struct { } // runListCommand detects and lists the connected arduino boards -// (either via serial or network ports). func runListCommand(cmd *cobra.Command, args []string) { pm := commands.InitPackageManager() - monitor := discovery.New(time.Millisecond) - monitor.Start() - duration, err := time.ParseDuration(listFlags.timeout) + timeout, err := time.ParseDuration(listFlags.timeout) if err != nil { - duration = time.Second * 5 + formatter.PrintError(err, "Invalid timeout.") + os.Exit(commands.ErrBadArgument) } - if formatter.IsCurrentFormat("text") { - stoppable := cc.Run(func(stop chan struct{}) { - for { - select { - case <-stop: - fmt.Print("\r \r") - return - default: - fmt.Print("\rDiscovering. ") - time.Sleep(time.Millisecond * 500) - fmt.Print("\rDiscovering.. ") - time.Sleep(time.Millisecond * 500) - fmt.Print("\rDiscovering...") - time.Sleep(time.Millisecond * 500) - } - } - }) - fmt.Print("\r") + // Check for bultin serial-discovery tool + loadBuiltinSerialDiscoveryMetadata(pm) + serialDiscoveryTool, _ := getBuiltinSerialDiscoveryTool(pm) + if !serialDiscoveryTool.IsInstalled() { + formatter.Print("Downloading and installing missing tool: " + serialDiscoveryTool.String()) + core.DownloadToolRelease(pm, serialDiscoveryTool) + core.InstallToolRelease(pm, serialDiscoveryTool) + + if err := pm.LoadHardware(commands.Config); err != nil { + formatter.PrintError(err, "Could not load hardware packages.") + os.Exit(commands.ErrCoreConfig) + } + serialDiscoveryTool, _ = getBuiltinSerialDiscoveryTool(pm) + if !serialDiscoveryTool.IsInstalled() { + formatter.PrintErrorMessage("Missing serial-discovery tool.") + os.Exit(commands.ErrCoreConfig) + } + } - time.Sleep(duration) - stoppable.Stop() - <-stoppable.Stopped - } else { - time.Sleep(duration) + serialDiscovery, err := discovery.NewFromCommandLine(serialDiscoveryTool.InstallDir.Join("serial-discovery").String()) + if err != nil { + formatter.PrintError(err, "Error setting up serial-discovery tool.") + os.Exit(commands.ErrCoreConfig) } - formatter.Print(NewBoardList(pm, monitor)) + // Find all installed discoveries + discoveries := discovery.ExtractDiscoveriesFromPlatforms(pm) + discoveries["serial"] = serialDiscovery - //monitor.Stop() //If called will slow like 1sec the program to close after print, with the same result (tested). - // it closes ungracefully, but at the end of the command we can't have races. -} + res := []*detectedPort{} + for discName, disc := range discoveries { + disc.Timeout = timeout + disc.Start() + defer disc.Close() -// NewBoardList returns a new board list by adding discovered boards from the board list and a monitor. -func NewBoardList(pm *packagemanager.PackageManager, monitor *discovery.Monitor) *output.AttachedBoardList { - if monitor == nil { - return nil + ports, err := disc.List() + if err != nil { + fmt.Printf("Error getting port list from discovery %s: %s\n", discName, err) + continue + } + for _, port := range ports { + b := detectedBoards{} + for _, board := range pm.IdentifyBoard(port.IdentificationPrefs) { + b = append(b, &detectedBoard{ + Name: board.Name(), + FQBN: board.FQBN(), + }) + } + p := &detectedPort{ + Address: port.Address, + Protocol: port.Protocol, + ProtocolLabel: port.ProtocolLabel, + Boards: b, + } + res = append(res, p) + } } + output.Emit(&detectedPorts{ + Ports: res, + }) +} + +type detectedPorts struct { + Ports []*detectedPort `json:"ports"` +} + +type detectedPort struct { + Address string `json:"address"` + Protocol string `json:"protocol"` + ProtocolLabel string `json:"protocol_label"` + Boards detectedBoards `json:"boards"` +} + +type detectedBoards []*detectedBoard + +type detectedBoard struct { + Name string `json:"name"` + FQBN string `json:"fqbn"` +} - serialDevices := monitor.Serial() - networkDevices := monitor.Network() - ret := &output.AttachedBoardList{ - SerialBoards: make([]output.SerialBoardListItem, 0, len(serialDevices)), - NetworkBoards: make([]output.NetworkBoardListItem, 0, len(networkDevices)), +func (b detectedBoards) Less(i, j int) bool { + x := b[i] + y := b[j] + if x.Name < y.Name { + return true } + return x.FQBN < y.FQBN +} - for _, item := range serialDevices { - boards := pm.FindBoardsWithVidPid(item.VendorID, item.ProductID) - if len(boards) == 0 { - ret.SerialBoards = append(ret.SerialBoards, output.SerialBoardListItem{ - Name: "unknown", - Port: item.Port, - UsbID: fmt.Sprintf("%s:%s - %s", item.VendorID[2:], item.ProductID[2:], item.SerialNumber), - }) - continue - } +func (p detectedPorts) Less(i, j int) bool { + x := p.Ports[i] + y := p.Ports[j] + if x.Protocol < y.Protocol { + return true + } + if x.Address < y.Address { + return true + } + return false +} - board := boards[0] - ret.SerialBoards = append(ret.SerialBoards, output.SerialBoardListItem{ - Name: board.Name(), - Fqbn: board.FQBN(), - Port: item.Port, - UsbID: fmt.Sprintf("%s:%s - %s", item.VendorID[2:], item.ProductID[2:], item.SerialNumber), - }) +func (p detectedPorts) EmitJSON() string { + d, err := json.MarshalIndent(p, "", " ") + if err != nil { + formatter.PrintError(err, "Error encoding json") + os.Exit(commands.ErrGeneric) } + return string(d) +} - for _, item := range networkDevices { - boards := pm.FindBoardsWithID(item.Name) - if len(boards) == 0 { - // skip it if not recognized - continue +func (p detectedPorts) EmitTerminal() string { + sort.Slice(p.Ports, p.Less) + table := output.NewTable() + table.SetHeader("Port", "Type", "Board Name", "FQBN") + for _, port := range p.Ports { + address := port.Protocol + "://" + port.Address + if port.Protocol == "serial" { + address = port.Address + } + protocol := port.ProtocolLabel + if len(port.Boards) > 0 { + sort.Slice(port.Boards, port.Boards.Less) + for _, b := range port.Boards { + board := b.Name + fqbn := b.FQBN + table.AddRow(address, protocol, board, fqbn) + // show address and protocol only on the first row + address = "" + protocol = "" + } + } else { + board := "Unknown" + fqbn := "" + table.AddRow(address, protocol, board, fqbn) } - - board := boards[0] - ret.NetworkBoards = append(ret.NetworkBoards, output.NetworkBoardListItem{ - Name: board.Name(), - Fqbn: board.FQBN(), - Location: fmt.Sprintf("%s:%d", item.Address, item.Port), - }) } - return ret + return table.Render() } diff --git a/commands/board/serial.go b/commands/board/serial.go new file mode 100644 index 00000000000..172dd4a4747 --- /dev/null +++ b/commands/board/serial.go @@ -0,0 +1,89 @@ +// +// This file is part of arduino-cli. +// +// Copyright 2018 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to modify or +// otherwise use the software for commercial activities involving the Arduino +// software without disclosing the source code of your own applications. To purchase +// a commercial license, send an email to license@arduino.cc. +// + +package board + +import ( + "github.com/arduino/arduino-cli/arduino/cores" + "github.com/arduino/arduino-cli/arduino/cores/packagemanager" + "github.com/arduino/arduino-cli/arduino/resources" + semver "go.bug.st/relaxed-semver" +) + +var serialDiscoveryVersion = semver.ParseRelaxed("0.5.0") + +func loadBuiltinSerialDiscoveryMetadata(pm *packagemanager.PackageManager) { + builtinPackage := pm.GetPackages().GetOrCreatePackage("builtin") + ctagsTool := builtinPackage.GetOrCreateTool("serial-discovery") + ctagsRel := ctagsTool.GetOrCreateRelease(serialDiscoveryVersion) + ctagsRel.Flavors = []*cores.Flavor{ + // { + // OS: "i686-pc-linux-gnu", + // Resource: &resources.DownloadResource{ + // ArchiveFileName: "serial-discovery-1.0.0-i686-pc-linux-gnu.tar.bz2", + // URL: "https://downloads.arduino.cc/tools/serial-discovery-1.0.0-i686-pc-linux-gnu.tar.bz2", + // Size: , + // Checksum: "SHA-256:", + // CachePath: "tools", + // }, + // }, + { + OS: "x86_64-pc-linux-gnu", + Resource: &resources.DownloadResource{ + ArchiveFileName: "serial-discovery-0.5.0-x86_64-pc-linux-gnu.tar.bz2", + URL: "https://downloads.arduino.cc/tools/serial-discovery-0.5.0-x86_64-pc-linux-gnu.tar.bz2", + Size: 1507380, + Checksum: "SHA-256:473cdd9e9f189cfd507b1f6c312d767513da11ec87cdbff1610153d6285e15ce", + CachePath: "tools", + }, + }, + // { + // OS: "i686-mingw32", + // Resource: &resources.DownloadResource{ + // ArchiveFileName: "serial-discovery-1.0.0-i686-mingw32.zip", + // URL: "https://downloads.arduino.cc/tools/serial-discovery-1.0.0-i686-mingw32.zip", + // Size: , + // Checksum: "SHA-256:", + // CachePath: "tools", + // }, + // }, + // { + // OS: "x86_64-apple-darwin", + // Resource: &resources.DownloadResource{ + // ArchiveFileName: "serial-discovery-1.0.0-x86_64-apple-darwin.zip", + // URL: "https://downloads.arduino.cc/tools/serial-discovery-1.0.0-x86_64-apple-darwin.zip", + // Size: , + // Checksum: "SHA-256:", + // CachePath: "tools", + // }, + // }, + // { + // OS: "arm-linux-gnueabihf", + // Resource: &resources.DownloadResource{ + // ArchiveFileName: "serial-discovery-1.0.0-armv6-linux-gnueabihf.tar.bz2", + // URL: "https://downloads.arduino.cc/tools/serial-discovery-1.0.0-armv6-linux-gnueabihf.tar.bz2", + // Size: , + // Checksum: "SHA-256:", + // CachePath: "tools", + // }, + // }, + } +} + +func getBuiltinSerialDiscoveryTool(pm *packagemanager.PackageManager) (*cores.ToolRelease, error) { + return pm.Package("builtin").Tool("serial-discovery").Release(serialDiscoveryVersion).Get() +}