diff --git a/config/config.ini b/config/config.ini index f63377db..65de6012 100644 --- a/config/config.ini +++ b/config/config.ini @@ -7,4 +7,4 @@ updateUrl = https://downloads.arduino.cc/ origins = https://local.arduino.cc:8000 #httpProxy = http://your.proxy:port # Proxy server for HTTP requests crashreport = false # enable crashreport logging -autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent) \ No newline at end of file +autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent) diff --git a/conn.go b/conn.go index 8c71c54c..75486bf9 100644 --- a/conn.go +++ b/conn.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" + "github.com/arduino/arduino-create-agent/tools" "github.com/arduino/arduino-create-agent/upload" "github.com/arduino/arduino-create-agent/utilities" "github.com/gin-gonic/gin" @@ -80,7 +81,7 @@ type Upload struct { var uploadStatusStr = "ProgrammerStatus" -func uploadHandler(pubKey *rsa.PublicKey) func(*gin.Context) { +func uploadHandler(hub *hub, pubKey *rsa.PublicKey, tools *tools.Tools) func(*gin.Context) { return func(c *gin.Context) { data := new(Upload) if err := c.BindJSON(data); err != nil { @@ -162,28 +163,28 @@ func uploadHandler(pubKey *rsa.PublicKey) func(*gin.Context) { go func() { // Resolve commandline - commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools) + commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, tools) if err != nil { - send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) + send(hub, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) return } - l := PLogger{Verbose: true} + l := PLogger{Verbose: true, hub: hub} // Upload if data.Extra.Network { err = errors.New("network upload is not supported anymore, pease use OTA instead") } else { - send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"}) + send(hub, map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"}) err = upload.Serial(data.Port, commandline, data.Extra, l) } // Handle result if err != nil { - send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) + send(hub, map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) return } - send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"}) + send(hub, map[string]string{uploadStatusStr: "Done", "Flash": "Ok"}) }() c.String(http.StatusAccepted, "") @@ -193,6 +194,7 @@ func uploadHandler(pubKey *rsa.PublicKey) func(*gin.Context) { // PLogger sends the info from the upload to the websocket type PLogger struct { Verbose bool + hub *hub } // Debug only sends messages if verbose is true (always true for now) @@ -206,15 +208,15 @@ func (l PLogger) Debug(args ...interface{}) { func (l PLogger) Info(args ...interface{}) { output := fmt.Sprint(args...) log.Println(output) - send(map[string]string{uploadStatusStr: "Busy", "Msg": output}) + send(l.hub, map[string]string{uploadStatusStr: "Busy", "Msg": output}) } -func send(args map[string]string) { +func send(h *hub, args map[string]string) { mapB, _ := json.Marshal(args) h.broadcastSys <- mapB } -func wsHandler() *WsServer { +func wsHandler(h *hub) *WsServer { server, err := socketio.NewServer(nil) if err != nil { log.Fatal(err) diff --git a/hub.go b/hub.go index a162dd01..fabd408b 100755 --- a/hub.go +++ b/hub.go @@ -25,7 +25,10 @@ import ( "runtime/debug" "strconv" "strings" + "sync" + "github.com/arduino/arduino-create-agent/systray" + "github.com/arduino/arduino-create-agent/tools" "github.com/arduino/arduino-create-agent/upload" log "github.com/sirupsen/logrus" ) @@ -45,14 +48,51 @@ type hub struct { // Unregister requests from connections. unregister chan *connection + + // Serial hub to communicate with serial ports + serialHub *serialhub + + serialPortList *SerialPortList + + tools *tools.Tools + + systray *systray.Systray + + // This lock is used to prevent multiple threads from trying to open the same port at the same time. + // It presents issues with the serial port driver on some OS's: https://github.com/arduino/arduino-create-agent/issues/1031 + spHandlerOpenLock sync.Mutex } -var h = hub{ - broadcast: make(chan []byte, 1000), - broadcastSys: make(chan []byte, 1000), - register: make(chan *connection), - unregister: make(chan *connection), - connections: make(map[*connection]bool), +func newHub(tools *tools.Tools, systray *systray.Systray) *hub { + broadcastSys := make(chan []byte, 1000) + + onRegister := func(port *serport, msg string) { + broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"" + msg + "\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}") + } + onUnregister := func(port *serport) { + broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}") + } + serialHubub := newSerialHub(onRegister, onUnregister) + + onList := func(data []byte) { + broadcastSys <- data + } + onErr := func(err string) { + broadcastSys <- []byte("{\"Error\":\"" + err + "\"}") + } + serialPortList := newSerialPortList(tools, onList, onErr) + + return &hub{ + broadcast: make(chan []byte, 1000), + broadcastSys: broadcastSys, + register: make(chan *connection), + unregister: make(chan *connection), + connections: make(map[*connection]bool), + serialHub: serialHubub, + serialPortList: serialPortList, + tools: tools, + systray: systray, + } } const commands = `{ @@ -95,6 +135,8 @@ func (h *hub) sendToRegisteredConnections(data []byte) { } func (h *hub) run() { + go h.serialPortList.Run() + for { select { case c := <-h.register: @@ -108,7 +150,7 @@ func (h *hub) run() { h.unregisterConnection(c) case m := <-h.broadcast: if len(m) > 0 { - checkCmd(m) + h.checkCmd(m) h.sendToRegisteredConnections(m) } case m := <-h.broadcastSys: @@ -117,7 +159,7 @@ func (h *hub) run() { } } -func checkCmd(m []byte) { +func (h *hub) checkCmd(m []byte) { //log.Print("Inside checkCmd") s := string(m[:]) @@ -132,18 +174,18 @@ func checkCmd(m []byte) { args := strings.Split(s, " ") if len(args) < 3 { - go spErr("You did not specify a port and baud rate in your open cmd") + go h.spErr("You did not specify a port and baud rate in your open cmd") return } if len(args[1]) < 1 { - go spErr("You did not specify a serial port") + go h.spErr("You did not specify a serial port") return } baudStr := strings.Replace(args[2], "\n", "", -1) baud, err := strconv.Atoi(baudStr) if err != nil { - go spErr("Problem converting baud rate " + args[2]) + go h.spErr("Problem converting baud rate " + args[2]) return } // pass in buffer type now as string. if user does not @@ -154,15 +196,15 @@ func checkCmd(m []byte) { buftype := strings.Replace(args[3], "\n", "", -1) bufferAlgorithm = buftype } - go spHandlerOpen(args[1], baud, bufferAlgorithm) + go h.spHandlerOpen(args[1], baud, bufferAlgorithm) } else if strings.HasPrefix(sl, "close") { args := strings.Split(s, " ") if len(args) > 1 { - go spClose(args[1]) + go h.spClose(args[1]) } else { - go spErr("You did not specify a port to close") + go h.spErr("You did not specify a port to close") } } else if strings.HasPrefix(sl, "killupload") { @@ -175,9 +217,9 @@ func checkCmd(m []byte) { } else if strings.HasPrefix(sl, "send") { // will catch send and sendnobuf and sendraw - go spWrite(s) + go h.spWrite(s) } else if strings.HasPrefix(sl, "list") { - go serialPorts.List() + go h.serialPortList.List() } else if strings.HasPrefix(sl, "downloadtool") { go func() { args := strings.Split(s, " ") @@ -208,7 +250,12 @@ func checkCmd(m []byte) { behaviour = args[4] } - err := Tools.Download(pack, tool, toolVersion, behaviour) + reportPendingProgress := func(msg string) { + mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg} + mapB, _ := json.Marshal(mapD) + h.broadcastSys <- mapB + } + err := h.tools.Download(pack, tool, toolVersion, behaviour, reportPendingProgress) if err != nil { mapD := map[string]string{"DownloadStatus": "Error", "Msg": err.Error()} mapB, _ := json.Marshal(mapD) @@ -220,29 +267,41 @@ func checkCmd(m []byte) { } }() } else if strings.HasPrefix(sl, "log") { - go logAction(sl) + go h.logAction(sl) } else if strings.HasPrefix(sl, "restart") { + // potentially, the sysStray dependencies can be removed https://github.com/arduino/arduino-create-agent/issues/1013 log.Println("Received restart from the daemon. Why? Boh") - Systray.Restart() + h.systray.Restart() } else if strings.HasPrefix(sl, "exit") { - Systray.Quit() + h.systray.Quit() } else if strings.HasPrefix(sl, "memstats") { - memoryStats() + h.memoryStats() } else if strings.HasPrefix(sl, "gc") { - garbageCollection() + h.garbageCollection() } else if strings.HasPrefix(sl, "hostname") { - getHostname() + h.getHostname() } else if strings.HasPrefix(sl, "version") { - getVersion() + h.getVersion() } else { - go spErr("Could not understand command.") + go h.spErr("Could not understand command.") } } -func logAction(sl string) { +// ChanWriter is a simple io.Writer that sends data to a channel. +type ChanWriter struct { + Ch chan<- []byte +} + +func (u *ChanWriter) Write(p []byte) (n int, err error) { + u.Ch <- p + return len(p), nil +} + +func (h *hub) logAction(sl string) { if strings.HasPrefix(sl, "log on") { *logDump = "on" - multiWriter := io.MultiWriter(&loggerWs, os.Stderr) + + multiWriter := io.MultiWriter(&ChanWriter{Ch: h.broadcastSys}, os.Stderr) log.SetOutput(multiWriter) } else if strings.HasPrefix(sl, "log off") { *logDump = "off" @@ -253,7 +312,7 @@ func logAction(sl string) { } } -func memoryStats() { +func (h *hub) memoryStats() { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) json, _ := json.Marshal(memStats) @@ -261,22 +320,22 @@ func memoryStats() { h.broadcastSys <- json } -func getHostname() { +func (h *hub) getHostname() { h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}") } -func getVersion() { +func (h *hub) getVersion() { h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}") } -func garbageCollection() { +func (h *hub) garbageCollection() { log.Printf("Starting garbageCollection()\n") h.broadcastSys <- []byte("{\"gc\":\"starting\"}") - memoryStats() + h.memoryStats() debug.SetGCPercent(100) debug.FreeOSMemory() debug.SetGCPercent(-1) log.Printf("Done with garbageCollection()\n") h.broadcastSys <- []byte("{\"gc\":\"done\"}") - memoryStats() + h.memoryStats() } diff --git a/info.go b/info.go index 88145c02..2d629671 100644 --- a/info.go +++ b/info.go @@ -19,6 +19,7 @@ import ( "runtime" "strings" + "github.com/arduino/arduino-create-agent/systray" "github.com/gin-gonic/gin" "go.bug.st/serial" ) @@ -40,14 +41,16 @@ func infoHandler(c *gin.Context) { }) } -func pauseHandler(c *gin.Context) { - go func() { - ports, _ := serial.GetPortsList() - for _, element := range ports { - spClose(element) - } - *hibernate = true - Systray.Pause() - }() - c.JSON(200, nil) +func pauseHandler(hub *hub, s *systray.Systray) func(c *gin.Context) { + return func(c *gin.Context) { + go func() { + ports, _ := serial.GetPortsList() + for _, element := range ports { + hub.spClose(element) + } + *hibernate = true + s.Pause() + }() + c.JSON(200, nil) + } } diff --git a/main.go b/main.go index 41f824b1..51a26450 100755 --- a/main.go +++ b/main.go @@ -20,7 +20,6 @@ package main import ( _ "embed" - "encoding/json" "flag" "html/template" "io" @@ -100,22 +99,6 @@ var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHTML)) //go:embed home.html var homeTemplateHTML string -// global clients -var ( - Tools *tools.Tools - Systray systray.Systray - Index *index.Resource -) - -type logWriter struct{} - -func (u *logWriter) Write(p []byte) (n int, err error) { - h.broadcastSys <- p - return len(p), nil -} - -var loggerWs logWriter - func homeHandler(c *gin.Context) { homeTemplate.Execute(c.Writer, c.Request.Host) } @@ -141,12 +124,9 @@ func main() { // Check if certificates made with Agent <=1.2.7 needs to be moved over the new location cert.MigrateCertificatesGeneratedWithOldAgentVersions(config.GetCertificatesDir()) - // Launch main loop in a goroutine - go loop() - // SetupSystray is the main thread configDir := config.GetDefaultConfigDir() - Systray = systray.Systray{ + stray := systray.Systray{ Hibernate: *hibernate, Version: version + "-" + commit, DebugURL: func() string { @@ -156,16 +136,19 @@ func main() { ConfigDir: configDir, } + // Launch main loop in a goroutine + go loop(&stray) + if src, err := os.Executable(); err != nil { panic(err) } else if restartPath := updater.Start(src); restartPath != "" { - Systray.RestartWith(restartPath) + stray.RestartWith(restartPath) } else { - Systray.Start() + stray.Start() } } -func loop() { +func loop(stray *systray.Systray) { if *hibernate { return } @@ -182,11 +165,18 @@ func loop() { os.Exit(0) } - logger := func(msg string) { - mapD := map[string]string{"DownloadStatus": "Pending", "Msg": msg} - mapB, _ := json.Marshal(mapD) - h.broadcastSys <- mapB + // Instantiate Index and Tools + index := index.Init(*indexURL, config.GetDataDir()) + if signatureKey == nil || len(*signatureKey) == 0 { + log.Panicf("signature public key should be set") } + signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey)) + if err != nil { + log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err) + } + tools := tools.New(config.GetDataDir(), index, signaturePubKey) + + hub := newHub(tools, stray) // Let's handle the config configDir := config.GetDefaultConfigDir() @@ -258,7 +248,7 @@ func loop() { if err != nil { log.Panicf("cannot parse arguments: %s", err) } - Systray.SetCurrentConfigFile(configPath) + stray.SetCurrentConfigFile(configPath) // Parse additional ini config if defined if len(*additionalConfig) > 0 { @@ -278,18 +268,6 @@ func loop() { } } - if signatureKey == nil || len(*signatureKey) == 0 { - log.Panicf("signature public key should be set") - } - signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey)) - if err != nil { - log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err) - } - - // Instantiate Index and Tools - Index = index.Init(*indexURL, config.GetDataDir()) - Tools = tools.New(config.GetDataDir(), Index, logger, signaturePubKey) - // see if we are supposed to wait 5 seconds if *isLaunchSelf { launchSelfLater() @@ -414,16 +392,14 @@ func loop() { } } - // launch the discoveries for the running system - go serialPorts.Run() // launch the hub routine which is the singleton for the websocket server - go h.run() + go hub.run() // launch our dummy data routine //go d.run() r := gin.New() - socketHandler := wsHandler().ServeHTTP + socketHandler := wsHandler(hub).ServeHTTP extraOrigins := []string{ "https://create.arduino.cc", @@ -462,17 +438,17 @@ func loop() { r.LoadHTMLFiles("templates/nofirefox.html") r.GET("/", homeHandler) - r.POST("/upload", uploadHandler(signaturePubKey)) + r.POST("/upload", uploadHandler(hub, signaturePubKey, tools)) r.GET("/socket.io/", socketHandler) r.POST("/socket.io/", socketHandler) r.Handle("WS", "/socket.io/", socketHandler) r.Handle("WSS", "/socket.io/", socketHandler) r.GET("/info", infoHandler) - r.POST("/pause", pauseHandler) - r.POST("/update", updateHandler) + r.POST("/pause", pauseHandler(hub, stray)) + r.POST("/update", updateHandler(stray)) // Mount goa handlers - goa := v2.Server(config.GetDataDir().String(), Index, signaturePubKey) + goa := v2.Server(config.GetDataDir().String(), index, signaturePubKey) r.Any("/v2/*path", gin.WrapH(goa)) go func() { diff --git a/main_test.go b/main_test.go index 1387fd22..4b0720e0 100644 --- a/main_test.go +++ b/main_test.go @@ -29,9 +29,11 @@ import ( "testing" "github.com/arduino/arduino-create-agent/config" - "github.com/arduino/arduino-create-agent/gen/tools" + genTools "github.com/arduino/arduino-create-agent/gen/tools" "github.com/arduino/arduino-create-agent/globals" "github.com/arduino/arduino-create-agent/index" + "github.com/arduino/arduino-create-agent/systray" + "github.com/arduino/arduino-create-agent/tools" "github.com/arduino/arduino-create-agent/upload" "github.com/arduino/arduino-create-agent/utilities" v2 "github.com/arduino/arduino-create-agent/v2" @@ -56,7 +58,15 @@ func TestValidSignatureKey(t *testing.T) { func TestUploadHandlerAgainstEvilFileNames(t *testing.T) { r := gin.New() - r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))) + + index := index.Init(*indexURL, config.GetDataDir()) + signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey)) + require.NoError(t, err) + tools := tools.New(config.GetDataDir(), index, signaturePubKey) + hub := newHub(tools, &systray.Systray{}) + pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)) + + r.POST("/", uploadHandler(hub, pubkey, tools)) ts := httptest.NewServer(r) uploadEvilFileName := Upload{ @@ -92,7 +102,15 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) { func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) { r := gin.New() - r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))) + + index := index.Init(*indexURL, config.GetDataDir()) + signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey)) + require.NoError(t, err) + tools := tools.New(config.GetDataDir(), index, signaturePubKey) + hub := newHub(tools, &systray.Systray{}) + pubkey := utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)) + + r.POST("/", uploadHandler(hub, pubkey, tools)) ts := httptest.NewServer(r) defer ts.Close() @@ -126,7 +144,7 @@ func TestInstallToolV2(t *testing.T) { ts := httptest.NewServer(r) type test struct { - request tools.ToolPayload + request genTools.ToolPayload responseCode int responseBody string } @@ -134,7 +152,7 @@ func TestInstallToolV2(t *testing.T) { bossacURL := "http://downloads.arduino.cc/tools/bossac-1.7.0-arduino3-linux64.tar.gz" bossacChecksum := "SHA-256:1ae54999c1f97234a5c603eb99ad39313b11746a4ca517269a9285afa05f9100" bossacSignature := "382898a97b5a86edd74208f10107d2fecbf7059ffe9cc856e045266fb4db4e98802728a0859cfdcda1c0b9075ec01e42dbea1f430b813530d5a6ae1766dfbba64c3e689b59758062dc2ab2e32b2a3491dc2b9a80b9cda4ae514fbe0ec5af210111b6896976053ab76bac55bcecfcececa68adfa3299e3cde6b7f117b3552a7d80ca419374bb497e3c3f12b640cf5b20875416b45e662fc6150b99b178f8e41d6982b4c0a255925ea39773683f9aa9201dc5768b6fc857c87ff602b6a93452a541b8ec10ca07f166e61a9e9d91f0a6090bd2038ed4427af6251039fb9fe8eb62ec30d7b0f3df38bc9de7204dec478fb86f8eb3f71543710790ee169dce039d3e0" - bossacInstallURLOK := tools.ToolPayload{ + bossacInstallURLOK := genTools.ToolPayload{ Name: "bossac", Version: "1.7.0-arduino3", Packager: "arduino", @@ -146,7 +164,7 @@ func TestInstallToolV2(t *testing.T) { esptoolURL := "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-3/x86_64-linux-gnu.esptool-f80ae31.tar.gz" esptoolChecksum := "SHA-256:bded1dca953377838b6086a9bcd40a1dc5286ba5f69f2372c22a1d1819baad24" esptoolSignature := "852b58871419ce5e5633ecfaa72c0f0fa890ceb51164b362b8133bc0e3e003a21cec48935b8cdc078f4031219cbf17fb7edd9d7c9ca8ed85492911c9ca6353c9aa4691eb91fda99563a6bd49aeca0d9981fb05ec76e45c6024f8a6822862ad1e34ddc652fbbf4fa909887a255d4f087398ec386577efcec523c21203be3d10fc9e9b0f990a7536875a77dc2bc5cbffea7734b62238e31719111b718bacccebffc9be689545540e81d23b81caa66214376f58a0d6a45cf7efc5d3af62ab932b371628162fffe403906f41d5534921e5be081c5ac2ecc9db5caec03a105cc44b00ce19a95ad079843501eb8182e0717ce327867380c0e39d2b48698547fc1d0d66" - esptoolInstallURLOK := tools.ToolPayload{ + esptoolInstallURLOK := genTools.ToolPayload{ Name: "esptool", Version: "2.5.0-3-20ed2b9", Packager: "esp8266", @@ -156,7 +174,7 @@ func TestInstallToolV2(t *testing.T) { } wrongSignature := "wr0ngs1gn4tur3" - bossacInstallWrongSig := tools.ToolPayload{ + bossacInstallWrongSig := genTools.ToolPayload{ Name: "bossac", Version: "1.7.0-arduino3", Packager: "arduino", @@ -166,7 +184,7 @@ func TestInstallToolV2(t *testing.T) { } wrongChecksum := "wr0ngch3cksum" - bossacInstallWrongCheck := tools.ToolPayload{ + bossacInstallWrongCheck := genTools.ToolPayload{ Name: "bossac", Version: "1.7.0-arduino3", Packager: "arduino", @@ -175,7 +193,7 @@ func TestInstallToolV2(t *testing.T) { Signature: &bossacSignature, } - bossacInstallNoURL := tools.ToolPayload{ + bossacInstallNoURL := genTools.ToolPayload{ Name: "bossac", Version: "1.7.0-arduino3", Packager: "arduino", diff --git a/serial.go b/serial.go old mode 100755 new mode 100644 index 1a43f364..4721cdf2 --- a/serial.go +++ b/serial.go @@ -20,11 +20,11 @@ package main import ( "encoding/json" "slices" - "strconv" "strings" "sync" "time" + "github.com/arduino/arduino-create-agent/tools" discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2" "github.com/sirupsen/logrus" ) @@ -34,12 +34,27 @@ type serialhub struct { ports map[string]*serport mu sync.Mutex + + onRegister func(port *serport, msg string) + onUnregister func(port *serport) +} + +func newSerialHub(onRegister func(port *serport, msg string), onUnregister func(port *serport)) *serialhub { + return &serialhub{ + ports: make(map[string]*serport), + onRegister: onRegister, + onUnregister: onUnregister, + } } // SerialPortList is the serial port list type SerialPortList struct { Ports []*SpPortItem portsLock sync.Mutex + + tools *tools.Tools `json:"-"` + OnList func([]byte) `json:"-"` + OnErr func(string) `json:"-"` } // SpPortItem is the serial port item @@ -56,18 +71,10 @@ type SpPortItem struct { ProductID string } -// serialPorts contains the ports attached to the machine -var serialPorts SerialPortList - -var sh = serialhub{ - ports: make(map[string]*serport), -} - // Register serial ports from the connections. func (sh *serialhub) Register(port *serport) { sh.mu.Lock() - //log.Print("Registering a port: ", p.portConf.Name) - h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}") + sh.onRegister(port, "Got register/open on port.") sh.ports[port.portName] = port sh.mu.Unlock() } @@ -75,8 +82,7 @@ func (sh *serialhub) Register(port *serport) { // Unregister requests from connections. func (sh *serialhub) Unregister(port *serport) { sh.mu.Lock() - //log.Print("Unregistering a port: ", p.portConf.Name) - h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}") + sh.onUnregister(port) delete(sh.ports, port.portName) close(port.sendBuffered) close(port.sendNoBuf) @@ -86,8 +92,22 @@ func (sh *serialhub) Unregister(port *serport) { func (sh *serialhub) FindPortByName(portname string) (*serport, bool) { sh.mu.Lock() defer sh.mu.Unlock() - port, ok := sh.ports[portname] - return port, ok + + for name, port := range sh.ports { + if strings.EqualFold(name, portname) { + // we found our port + return port, true + } + } + return nil, false +} + +func newSerialPortList(tools *tools.Tools, onList func(data []byte), onErr func(err string)) *SerialPortList { + return &SerialPortList{ + tools: tools, + OnList: onList, + OnErr: onErr, + } } // List broadcasts a Json representation of the ports found @@ -97,11 +117,9 @@ func (sp *SerialPortList) List() { sp.portsLock.Unlock() if err != nil { - //log.Println(err) - h.broadcastSys <- []byte("Error creating json on port list " + - err.Error()) + sp.OnErr("Error creating json on port list " + err.Error()) } else { - h.broadcastSys <- ls + sp.OnList(ls) } } @@ -118,11 +136,12 @@ func (sp *SerialPortList) Run() { func (sp *SerialPortList) runSerialDiscovery() { // First ensure that all the discoveries are available - if err := Tools.Download("builtin", "serial-discovery", "latest", "keep"); err != nil { + noOpProgress := func(msg string) {} + if err := sp.tools.Download("builtin", "serial-discovery", "latest", "keep", noOpProgress); err != nil { logrus.Errorf("Error downloading serial-discovery: %s", err) panic(err) } - sd, err := Tools.GetLocation("serial-discovery") + sd, err := sp.tools.GetLocation("serial-discovery") if err != nil { logrus.Errorf("Error downloading serial-discovery: %s", err) panic(err) @@ -248,22 +267,21 @@ func (sp *SerialPortList) getPortByName(portname string) *SpPortItem { return nil } -func spErr(err string) { - //log.Println("Sending err back: ", err) - //h.broadcastSys <- []byte(err) +// FIXME: the spErr, spWrite, spClose should be moved to the 'hub.go' file +func (h *hub) spErr(err string) { h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}") } -func spClose(portname string) { - if myport, ok := sh.FindPortByName(portname); ok { +func (h *hub) spClose(portname string) { + if myport, ok := h.serialHub.FindPortByName(portname); ok { h.broadcastSys <- []byte("Closing serial port " + portname) myport.Close() } else { - spErr("We could not find the serial port " + portname + " that you were trying to close.") + h.spErr("We could not find the serial port " + portname + " that you were trying to close.") } } -func spWrite(arg string) { +func (h *hub) spWrite(arg string) { // we will get a string of comXX asdf asdf asdf //log.Println("Inside spWrite arg: " + arg) arg = strings.TrimPrefix(arg, " ") @@ -272,7 +290,7 @@ func spWrite(arg string) { if len(args) != 3 { errstr := "Could not parse send command: " + arg //log.Println(errstr) - spErr(errstr) + h.spErr(errstr) return } bufferingMode := args[0] @@ -283,10 +301,10 @@ func spWrite(arg string) { //log.Println("The data is:" + data + "---") // See if we have this port open - port, ok := sh.FindPortByName(portname) + port, ok := h.serialHub.FindPortByName(portname) if !ok { // we couldn't find the port, so send err - spErr("We could not find the serial port " + portname + " that you were trying to write to.") + h.spErr("We could not find the serial port " + portname + " that you were trying to write to.") return } @@ -295,7 +313,7 @@ func spWrite(arg string) { case "send", "sendnobuf", "sendraw": // valid buffering mode, go ahead default: - spErr("Unsupported send command:" + args[0] + ". Please specify a valid one") + h.spErr("Unsupported send command:" + args[0] + ". Please specify a valid one") return } diff --git a/serialport.go b/serialport.go index b3418fe5..3b965bbb 100755 --- a/serialport.go +++ b/serialport.go @@ -20,13 +20,12 @@ import ( "encoding/base64" "io" "strconv" - "sync" "sync/atomic" "time" "unicode/utf8" log "github.com/sirupsen/logrus" - serial "go.bug.st/serial" + "go.bug.st/serial" ) // SerialConfig is the serial port configuration @@ -62,6 +61,9 @@ type serport struct { BufferType string //bufferwatcher *BufferflowDummypause bufferwatcher Bufferflow + + ChanWriter ChanWriter + OnClose func(*serport) } // SpPortMessage is the serial port message @@ -90,7 +92,7 @@ func (p *serport) reader(buftype string) { if p.isClosing.Load() { strmsg := "Shutting down reader on " + p.portConf.Name log.Println(strmsg) - h.broadcastSys <- []byte(strmsg) + p.ChanWriter.Write([]byte(strmsg)) break } @@ -144,15 +146,14 @@ func (p *serport) reader(buftype string) { if err == io.EOF || err == io.ErrUnexpectedEOF { // hit end of file log.Println("Hit end of file on serial port") - h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}") + p.ChanWriter.Write([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")) } if err != nil { log.Println(err) - h.broadcastSys <- []byte("Error reading on " + p.portConf.Name + " " + - err.Error() + " Closing port.") - h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}") + p.ChanWriter.Write([]byte("Error reading on " + p.portConf.Name + " " + err.Error() + " Closing port.")) + p.ChanWriter.Write([]byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}")) p.isClosingDueToError = true break } @@ -210,7 +211,7 @@ func (p *serport) writerBuffered() { } msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name log.Println(msgstr) - h.broadcastSys <- []byte(msgstr) + p.ChanWriter.Write([]byte(msgstr)) } // this method runs as its own thread because it's instantiated @@ -231,15 +232,22 @@ func (p *serport) writerNoBuf() { if err != nil { errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." log.Print(errstr) - h.broadcastSys <- []byte(errstr) + p.ChanWriter.Write([]byte(errstr)) break } } msgstr := "Shutting down writer on " + p.portConf.Name log.Println(msgstr) - h.broadcastSys <- []byte(msgstr) + p.ChanWriter.Write([]byte(msgstr)) + p.portIo.Close() - serialPorts.List() + + // NOTE: by removing the 'serialPorts.List()' line, + // the list of serial ports are NOT sent to the websocket clients after a write is completed. + // This should not be an issue since the list are periodically called. + // Note also that the 'writerBuffered' and 'writerRaw' methods do not call it. + // serialPorts.List() + } // this method runs as its own thread because it's instantiated @@ -271,16 +279,13 @@ func (p *serport) writerRaw() { } msgstr := "writerRaw just got closed. make sure you make a new one. port:" + p.portConf.Name log.Println(msgstr) - h.broadcastSys <- []byte(msgstr) + p.ChanWriter.Write([]byte(msgstr)) } -// This lock is used to prevent multiple threads from trying to open the same port at the same time. -// It presents issues with the serial port driver on some OS's: https://github.com/arduino/arduino-create-agent/issues/1031 -var spHandlerOpenLock sync.Mutex - -func spHandlerOpen(portname string, baud int, buftype string) { - spHandlerOpenLock.Lock() - defer spHandlerOpenLock.Unlock() +// FIXME: move this into the `hub.go` file +func (h *hub) spHandlerOpen(portname string, baud int, buftype string) { + h.spHandlerOpenLock.Lock() + defer h.spHandlerOpenLock.Unlock() log.Print("Inside spHandler") @@ -302,10 +307,10 @@ func spHandlerOpen(portname string, baud int, buftype string) { sp, err := serial.Open(portname, mode) log.Print("Just tried to open port") if err != nil { - existingPort, ok := sh.FindPortByName(portname) + existingPort, ok := h.serialHub.FindPortByName(portname) if ok && existingPort.portConf.Baud == baud && existingPort.BufferType == buftype { log.Print("Port already opened") - h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Port already opened.\",\"Port\":\"" + existingPort.portConf.Name + "\",\"Baud\":" + strconv.Itoa(existingPort.portConf.Baud) + ",\"BufferType\":\"" + existingPort.BufferType + "\"}") + h.serialHub.onRegister(existingPort, "Port already opened") } else { log.Print("Error opening port " + err.Error()) h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}") @@ -322,7 +327,14 @@ func spHandlerOpen(portname string, baud int, buftype string) { portConf: conf, portIo: sp, portName: portname, - BufferType: buftype} + BufferType: buftype, + ChanWriter: ChanWriter{h.broadcastSys}, + } + + p.OnClose = func(port *serport) { + h.serialPortList.MarkPortAsClosed(p.portName) + h.serialPortList.List() + } var bw Bufferflow @@ -340,10 +352,10 @@ func spHandlerOpen(portname string, baud int, buftype string) { bw.Init() p.bufferwatcher = bw - sh.Register(p) + h.serialHub.Register(p) - serialPorts.MarkPortAsOpened(portname) - serialPorts.List() + h.serialPortList.MarkPortAsOpened(portname) + h.serialPortList.List() // this is internally buffered thread to not send to serial port if blocked go p.writerBuffered() @@ -354,8 +366,8 @@ func spHandlerOpen(portname string, baud int, buftype string) { // this is the thread that reads from the serial port go func() { p.reader(buftype) - serialPorts.List() - sh.Unregister(p) + h.serialPortList.List() + h.serialHub.Unregister(p) }() } @@ -364,6 +376,5 @@ func (p *serport) Close() { p.bufferwatcher.Close() p.portIo.Close() - serialPorts.MarkPortAsClosed(p.portName) - serialPorts.List() + p.OnClose(p) } diff --git a/tools/download.go b/tools/download.go index 8c4a37a6..da7df680 100644 --- a/tools/download.go +++ b/tools/download.go @@ -42,7 +42,7 @@ import ( // If version is not "latest" and behaviour is "replace", it will download the // version again. If instead behaviour is "keep" it will not download the version // if it already exists. -func (t *Tools) Download(pack, name, version, behaviour string) error { +func (t *Tools) Download(pack, name, version, behaviour string, report func(msg string)) error { t.tools.SetBehaviour(behaviour) _, err := t.tools.Install(context.Background(), &tools.ToolPayload{Name: name, Version: version, Packager: pack}) @@ -58,16 +58,16 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { // if the tool contains a post_install script, run it: it means it is a tool that needs to install drivers // AFAIK this is only the case for the windows-driver tool - err = t.installDrivers(safePath) + err = t.installDrivers(safePath, report) if err != nil { return err } // Ensure that the files are executable - t.logger("Ensure that the files are executable") + report("Ensure that the files are executable") // Update the tool map - t.logger("Updating map with location " + safePath) + report("Updating map with location " + safePath) t.setMapValue(name, safePath) t.setMapValue(name+"-"+version, safePath) @@ -75,7 +75,7 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { return nil } -func (t *Tools) installDrivers(location string) error { +func (t *Tools) installDrivers(location string, report func(msg string)) error { OkPressed := 6 extension := ".bat" // add .\ to force locality @@ -86,11 +86,11 @@ func (t *Tools) installDrivers(location string) error { preamble = "./" } if _, err := os.Stat(filepath.Join(location, "post_install"+extension)); err == nil { - t.logger("Installing drivers") + report("Installing drivers") ok := MessageBox("Installing drivers", "We are about to install some drivers needed to use Arduino/Genuino boards\nDo you want to continue?") if ok == OkPressed { os.Chdir(location) - t.logger(preamble + "post_install" + extension) + report(preamble + "post_install" + extension) oscmd := exec.Command(preamble + "post_install" + extension) if runtime.GOOS != "linux" { // spawning a shell could be the only way to let the user type his password diff --git a/tools/download_test.go b/tools/download_test.go index 96a105fd..b99f1077 100644 --- a/tools/download_test.go +++ b/tools/download_test.go @@ -130,12 +130,12 @@ func TestDownload(t *testing.T) { IndexFile: *paths.New("testdata", "test_tool_index.json"), LastRefresh: time.Now(), } - testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) + testTools := New(tempDirPath, &testIndex, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) for _, tc := range testCases { t.Run(tc.name+"-"+tc.version, func(t *testing.T) { // Download the tool - err := testTools.Download("arduino-test", tc.name, tc.version, "replace") + err := testTools.Download("arduino-test", tc.name, tc.version, "replace", func(msg string) { t.Log(msg) }) require.NoError(t, err) // Check that the tool has been downloaded @@ -177,8 +177,8 @@ func TestCorruptedInstalled(t *testing.T) { defer fileJSON.Close() _, err = fileJSON.Write([]byte("Hello")) require.NoError(t, err) - testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) + testTools := New(tempDirPath, &testIndex, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) // Download the tool - err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep") + err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep", func(msg string) { t.Log(msg) }) require.NoError(t, err) } diff --git a/tools/tools.go b/tools/tools.go index f371126b..96265c2d 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -33,20 +33,18 @@ import ( // // - *directory* contains the location where the tools are downloaded. // - *indexURL* contains the url where the tools description is contained. -// - *logger* is a StdLogger used for reporting debug and info messages // - *installed* contains a map[string]string of the tools installed and their exact location // // Usage: // You have to call the New() function passing it the required parameters: // // index = index.Init("https://downloads.arduino.cc/packages/package_index.json", dataDir) -// tools := tools.New(dataDir, index, logger) +// tools := tools.New(dataDir, index) // Tools will represent the installed tools type Tools struct { directory *paths.Path index *index.Resource - logger func(msg string) installed map[string]string mutex sync.RWMutex tools *pkgs.Tools @@ -55,12 +53,10 @@ type Tools struct { // New will return a Tool object, allowing the caller to execute operations on it. // The New functions accept the directory to use to host the tools, // an index (used to download the tools), -// and a logger to log the operations -func New(directory *paths.Path, index *index.Resource, logger func(msg string), signPubKey *rsa.PublicKey) *Tools { +func New(directory *paths.Path, index *index.Resource, signPubKey *rsa.PublicKey) *Tools { t := &Tools{ directory: directory, index: index, - logger: logger, installed: map[string]string{}, mutex: sync.RWMutex{}, tools: pkgs.New(index, directory.String(), "replace", signPubKey), diff --git a/update.go b/update.go index 33c028bc..a12f1feb 100644 --- a/update.go +++ b/update.go @@ -30,20 +30,23 @@ package main import ( + "github.com/arduino/arduino-create-agent/systray" "github.com/arduino/arduino-create-agent/updater" "github.com/gin-gonic/gin" ) -func updateHandler(c *gin.Context) { - restartPath, err := updater.CheckForUpdates(version, *updateURL, *appName) - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) - if restartPath == "quit" { - Systray.Quit() - } else { - Systray.RestartWith(restartPath) +func updateHandler(s *systray.Systray) func(c *gin.Context) { + return func(c *gin.Context) { + restartPath, err := updater.CheckForUpdates(version, *updateURL, *appName) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) + if restartPath == "quit" { + s.Quit() + } else { + s.RestartWith(restartPath) + } } }