package main import ( "bufio" "bytes" "encoding/json" "fmt" log "github.com/Sirupsen/logrus" "github.com/facchinm/go-serial" "github.com/mattn/go-shellwords" "github.com/sfreiberg/simplessh" "github.com/xrash/smetrics" "io" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" ) var compiling = false func colonToUnderscore(input string) string { output := strings.Replace(input, ":", "_", -1) return output } type basicAuthData struct { UserName string Password string } type boardExtraInfo struct { use_1200bps_touch bool wait_for_upload_port bool networkPort bool authdata basicAuthData } // Scp uploads sourceFile to remote machine like native scp console app. func Scp(client *simplessh.Client, sourceFile, targetFile string) error { session, err := client.SSHClient.NewSession() if err != nil { return err } defer session.Close() src, srcErr := os.Open(sourceFile) if srcErr != nil { return srcErr } srcStat, statErr := src.Stat() if statErr != nil { return statErr } go func() { w, _ := session.StdinPipe() fmt.Fprintln(w, "C0644", srcStat.Size(), filepath.Base(targetFile)) if srcStat.Size() > 0 { io.Copy(w, src) fmt.Fprint(w, "\x00") w.Close() } else { fmt.Fprint(w, "\x00") w.Close() } }() if err := session.Run("scp -t " + targetFile); err != nil { return err } return nil } func spProgramSSHNetwork(portname string, boardname string, filePath string, commandline string, authdata basicAuthData) error { log.Println("Starting network upload") log.Println("Board Name: " + boardname) if authdata.UserName == "" { authdata.UserName = "root" } if authdata.Password == "" { authdata.Password = "arduino" } ssh_client, err := simplessh.ConnectWithPassword(portname+":22", authdata.UserName, authdata.Password) if err != nil { log.Println("Error connecting via ssh") return err } defer ssh_client.Close() err = Scp(ssh_client, filePath, "/tmp/sketch"+filepath.Ext(filePath)) if err != nil { log.Printf("Upload: %s\n", err) return err } if commandline == "" { // very special case for Yun (remove once AVR boards.txt is fixed) commandline = "merge-sketch-with-bootloader.lua /tmp/sketch.hex && /usr/bin/run-avrdude /tmp/sketch.hex" } fmt.Println(commandline) ssh_output, err := ssh_client.Exec(commandline) if err == nil { log.Printf("Flash: %s\n", ssh_output) mapD := map[string]string{"ProgrammerStatus": "Busy", "Msg": string(ssh_output)} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB } return err } func spProgramNetwork(portname string, boardname string, filePath string, authdata basicAuthData) error { log.Println("Starting network upload") log.Println("Board Name: " + boardname) if authdata.UserName == "" { authdata.UserName = "root" } if authdata.Password == "" { authdata.Password = "arduino" } // Prepare a form that you will submit to that URL. _url := "http://" + portname + "/data/upload_sketch_silent" var b bytes.Buffer w := multipart.NewWriter(&b) // Add your image file filePath = strings.Trim(filePath, "\n") f, err := os.Open(filePath) if err != nil { log.Println("Error opening file" + filePath + " err: " + err.Error()) return err } fw, err := w.CreateFormFile("sketch_hex", filePath) if err != nil { log.Println("Error creating form file") return err } if _, err = io.Copy(fw, f); err != nil { log.Println("Error copying form file") return err } // Add the other fields if fw, err = w.CreateFormField("board"); err != nil { log.Println("Error creating form field") return err } if _, err = fw.Write([]byte(colonToUnderscore(boardname))); err != nil { log.Println("Error writing form field") return err } // Don't forget to close the multipart writer. // If you don't close it, your request will be missing the terminating boundary. w.Close() // Now that you have a form, you can submit it to your handler. req, err := http.NewRequest("POST", _url, &b) if err != nil { log.Println("Error creating post request") return err } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) if authdata.UserName != "" { req.SetBasicAuth(authdata.UserName, authdata.Password) } //h.broadcastSys <- []byte("Start flashing with command " + cmdString) log.Printf("Network flashing on " + portname) mapD := map[string]string{"ProgrammerStatus": "Starting", "Cmd": "POST"} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB // Submit the request client := &http.Client{} res, err := client.Do(req) if err != nil { log.Println("Error during post request") return err } // Check the response if res.StatusCode != http.StatusOK { log.Errorf("bad status: %s", res.Status) err = fmt.Errorf("bad status: %s", res.Status) } return err } func spProgramLocal(portname string, boardname string, filePath string, commandline string, extraInfo boardExtraInfo) error { var err error if extraInfo.use_1200bps_touch { portname, err = touch_port_1200bps(portname, extraInfo.wait_for_upload_port) } if err != nil { log.Println("Could not touch the port") return err } log.Printf("Received commandline (unresolved):" + commandline) commandline = strings.Replace(commandline, "{build.path}", filepath.ToSlash(filepath.Dir(filePath)), 1) commandline = strings.Replace(commandline, "{build.project_name}", strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filepath.Base(filePath))), 1) commandline = strings.Replace(commandline, "{serial.port}", portname, 1) commandline = strings.Replace(commandline, "{serial.port.file}", filepath.Base(portname), 1) // search for runtime variables and replace with values from globalToolsMap var runtimeRe = regexp.MustCompile("\\{(.*?)\\}") runtimeVars := runtimeRe.FindAllString(commandline, -1) fmt.Println(runtimeVars) for _, element := range runtimeVars { // use string similarity to resolve a runtime var with a "similar" map element if globalToolsMap[element] == "" { max_similarity := 0.0 for i, candidate := range globalToolsMap { similarity := smetrics.Jaro(element, i) if similarity > 0.8 && similarity > max_similarity { max_similarity = similarity globalToolsMap[element] = candidate } } } commandline = strings.Replace(commandline, element, globalToolsMap[element], 1) } z, _ := shellwords.Parse(commandline) return spHandlerProgram(z[0], z[1:]) } func spProgramRW(portname string, boardname string, filePath string, commandline string, extraInfo boardExtraInfo) { compiling = true defer func() { time.Sleep(1500 * time.Millisecond) compiling = false }() var err error if extraInfo.networkPort { err = spProgramNetwork(portname, boardname, filePath, extraInfo.authdata) if err != nil { // no http method available, try ssh upload err = spProgramSSHNetwork(portname, boardname, filePath, commandline, extraInfo.authdata) } } else { err = spProgramLocal(portname, boardname, filePath, commandline, extraInfo) } if err != nil { log.Printf("Command finished with error: %v", err) mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board"} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB } else { log.Printf("Finished without error. Good stuff") mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok"} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB // analyze stdin } } var oscmd *exec.Cmd func spHandlerProgram(flasher string, cmdString []string) error { // if runtime.GOOS == "darwin" { // sh, _ := exec.LookPath("sh") // // prepend the flasher to run it via sh // cmdString = append([]string{flasher}, cmdString...) // oscmd = exec.Command(sh, cmdString...) // } else { // remove quotes form flasher command and cmdString flasher = strings.Replace(flasher, "\"", "", -1) for index, _ := range cmdString { cmdString[index] = strings.Replace(cmdString[index], "\"", "", -1) } extension := "" if runtime.GOOS == "windows" { extension = ".exe" } oscmd = exec.Command(flasher, cmdString...) tellCommandNotToSpawnShell(oscmd) stdout, err := oscmd.StdoutPipe() if err != nil { return err } stderr, err := oscmd.StderrPipe() if err != nil { return err } multi := io.MultiReader(stderr, stdout) // Stdout buffer //var cmdOutput []byte //h.broadcastSys <- []byte("Start flashing with command " + cmdString) log.Printf("Flashing with command:" + flasher + extension + " " + strings.Join(cmdString, " ")) mapD := map[string]string{"ProgrammerStatus": "Starting", "Cmd": strings.Join(cmdString, " ")} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB err = oscmd.Start() in := bufio.NewScanner(multi) in.Split(bufio.ScanLines) for in.Scan() { log.Info(in.Text()) mapD := map[string]string{"ProgrammerStatus": "Busy", "Msg": in.Text()} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB } err = oscmd.Wait() return err } func spHandlerProgramKill() { // Kill the process if there is one running if oscmd != nil && oscmd.Process.Pid > 0 { h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"PreKilled\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") oscmd.Process.Kill() h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"Killed\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") } else { if oscmd != nil { h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"KilledError\", \"Msg\": \"No current process\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") } else { h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"KilledError\", \"Msg\": \"No current process\"}") } } } func formatCmdline(cmdline string, boardOptions map[string]string) (string, bool) { list := strings.Split(cmdline, "{") if len(list) == 1 { return cmdline, false } cmdline = "" for _, item := range list { item_s := strings.Split(item, "}") item = boardOptions[item_s[0]] if len(item_s) == 2 { cmdline += item + item_s[1] } else { if item != "" { cmdline += item } else { cmdline += item_s[0] } } } log.Println(cmdline) return cmdline, true } func containsStr(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } func findNewPortName(slice1 []string, slice2 []string) string { m := map[string]int{} for _, s1Val := range slice1 { m[s1Val] = 1 } for _, s2Val := range slice2 { m[s2Val] = m[s2Val] + 1 } for mKey, mVal := range m { if mVal == 1 { return mKey } } return "" } func touch_port_1200bps(portname string, wait_for_upload_port bool) (string, error) { initialPortName := portname log.Println("Restarting in bootloader mode") before_reset_ports, _ := serial.GetPortsList() log.Println(before_reset_ports) var ports []string mode := &serial.Mode{ BaudRate: 1200, Vmin: 0, Vtimeout: 1, } port, err := serial.OpenPort(portname, mode) if err != nil { log.Println(err) return "", err } err = port.SetDTR(false) if err != nil { log.Println(err) } port.Close() timeout := false go func() { time.Sleep(10 * time.Second) timeout = true }() // wait for port to disappear if wait_for_upload_port { for { ports, _ = serial.GetPortsList() log.Println(ports) portname = findNewPortName(ports, before_reset_ports) if portname != "" { break } if timeout { break } time.Sleep(time.Millisecond * 100) } } // wait for port to reappear if wait_for_upload_port { after_reset_ports, _ := serial.GetPortsList() log.Println(after_reset_ports) for { ports, _ = serial.GetPortsList() log.Println(ports) portname = findNewPortName(ports, after_reset_ports) if portname != "" { break } if timeout { break } time.Sleep(time.Millisecond * 100) } } if portname == "" { portname = initialPortName } return portname, nil }