diff --git a/hub.go b/hub.go index 160fd6b7e..46aa9f679 100755 --- a/hub.go +++ b/hub.go @@ -1,19 +1,15 @@ package main import ( - "fmt" - "encoding/json" "io" "os" - "os/exec" "runtime" "runtime/debug" "strconv" "strings" "github.com/arduino/arduino-create-agent/upload" - "github.com/kardianos/osext" log "github.com/sirupsen/logrus" ) @@ -210,9 +206,9 @@ func checkCmd(m []byte) { go logAction(sl) } else if strings.HasPrefix(sl, "restart") { log.Println("Received restart from the daemon. Why? Boh") - restart("") + Systray.Restart() } else if strings.HasPrefix(sl, "exit") { - exit() + Systray.Quit() } else if strings.HasPrefix(sl, "memstats") { memoryStats() } else if strings.HasPrefix(sl, "gc") { @@ -267,51 +263,3 @@ func garbageCollection() { h.broadcastSys <- []byte("{\"gc\":\"done\"}") memoryStats() } - -func exit() { - quitSysTray() - log.Println("Starting new spjs process") - h.broadcastSys <- []byte("{\"Exiting\" : true}") - log.Fatal("Exited current spjs cuz asked to") - -} - -func restart(path string, args ...string) { - log.Println("called restart", path) - quitSysTray() - // relaunch ourself and exit - // the relaunch works because we pass a cmdline in - // that has serial-port-json-server only initialize 5 seconds later - // which gives us time to exit and unbind from serial ports and TCP/IP - // sockets like :8989 - log.Println("Starting new spjs process") - h.broadcastSys <- []byte("{\"Restarting\" : true}") - - // figure out current path of executable so we know how to restart - // this process using osext - exePath, err3 := osext.Executable() - if err3 != nil { - log.Printf("Error getting exe path using osext lib. err: %v\n", err3) - } - - if path == "" { - log.Printf("exePath using osext: %v\n", exePath) - } else { - exePath = path - } - - exePath = strings.Trim(exePath, "\n") - - args = append(args, "-ls") - args = append(args, "-hibernate="+fmt.Sprint(*hibernate)) - cmd := exec.Command(exePath, args...) - - err := cmd.Start() - if err != nil { - log.Printf("Got err restarting spjs: %v\n", err) - h.broadcastSys <- []byte("{\"Error\" : \"" + fmt.Sprintf("%v", err) + "\"}") - } else { - h.broadcastSys <- []byte("{\"Restarted\" : true}") - } - log.Fatal("Exited current spjs for restart") -} diff --git a/info.go b/info.go index c597c29d7..7dadebb9c 100644 --- a/info.go +++ b/info.go @@ -32,7 +32,7 @@ func pauseHandler(c *gin.Context) { spClose(element) } *hibernate = true - restart("") + Systray.Pause() }() c.JSON(200, nil) } diff --git a/main.go b/main.go index 5a6456a18..c45e270c8 100755 --- a/main.go +++ b/main.go @@ -15,9 +15,10 @@ import ( "text/template" "time" + "github.com/arduino/arduino-create-agent/systray" "github.com/arduino/arduino-create-agent/tools" "github.com/arduino/arduino-create-agent/utilities" - "github.com/arduino/arduino-create-agent/v2" + v2 "github.com/arduino/arduino-create-agent/v2" "github.com/gin-gonic/gin" "github.com/go-ini/ini" cors "github.com/itsjamie/gin-cors" @@ -67,7 +68,8 @@ var ( // global clients var ( - Tools tools.Tools + Tools tools.Tools + Systray systray.Systray ) type NullWriter int @@ -107,7 +109,16 @@ func main() { go loop() // SetupSystray is the main thread - setupSysTray() + Systray = systray.Systray{ + Hibernate: *hibernate, + Version: version + "-" + git_revision, + DebugURL: func() string { + return "http://" + *address + port + }, + AdditionalConfig: *additionalConfig, + } + + Systray.Start() } func loop() { diff --git a/systray/systray.go b/systray/systray.go new file mode 100644 index 000000000..3bf060fe3 --- /dev/null +++ b/systray/systray.go @@ -0,0 +1,76 @@ +package systray + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/kardianos/osext" +) + +// Systray manages the systray icon with its menu and actions. It also handles the pause/resume behaviour of the agent +type Systray struct { + // Whether the Agent is in Pause mode + Hibernate bool + // The version of the Agent, displayed in the trayicon menu + Version string + // The url of the debug page. It's a function because it could change port + DebugURL func() string + // The active configuration file + AdditionalConfig string + // The path of the exe (only used in update) + path string +} + +// Restart restarts the program +// it works by finding the executable path and launching it before quitting +func (s *Systray) Restart() { + if s.path == "" { + var err error + s.path, err = osext.Executable() + if err != nil { + fmt.Printf("Error getting exe path using osext lib. err: %v\n", err) + } + + // Trim newlines (needed on osx) + s.path = strings.Trim(s.path, "\n") + } + + // Build args + args := []string{"-ls", fmt.Sprintf("--hibernate=%v", s.Hibernate)} + + if s.AdditionalConfig != "" { + args = append(args, fmt.Sprintf("--additional-config=%s", s.AdditionalConfig)) + } + + fmt.Println(s.path, args) + + // Launch executable + cmd := exec.Command(s.path, args...) + err := cmd.Start() + if err != nil { + fmt.Printf("Error restarting process: %v\n", err) + return + } + + // If everything was fine, quit + s.Quit() +} + +// Pause restarts the program with the hibernate flag set to true +func (s *Systray) Pause() { + s.Hibernate = true + s.Restart() +} + +// Pause restarts the program with the hibernate flag set to false +func (s *Systray) Resume() { + s.Hibernate = false + s.Restart() +} + +// Update restarts the program with the given path +func (s *Systray) Update(path string) { + s.path = path + s.Restart() +} diff --git a/systray/systray_fake.go b/systray/systray_fake.go new file mode 100644 index 000000000..59a85b490 --- /dev/null +++ b/systray/systray_fake.go @@ -0,0 +1,14 @@ +// +build cli + +// Systray_fake gets compiled when the tag 'cli' is present. This is useful to build an agent without trayicon functionalities +package systray + +import "os" + +func (s *Systray) Start() { + select {} +} + +func (s *Systray) Quit() { + os.Exit(0) +} diff --git a/systray/systray_real.go b/systray/systray_real.go new file mode 100644 index 000000000..f32d1b7e8 --- /dev/null +++ b/systray/systray_real.go @@ -0,0 +1,160 @@ +// +build !cli + +// Systray_real gets compiled when the tag 'cli' is missing. This is the default case +package systray + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/arduino/arduino-create-agent/icon" + "github.com/getlantern/systray" + "github.com/go-ini/ini" + "github.com/kardianos/osext" + "github.com/skratchdot/open-golang/open" +) + +// Start sets up the systray icon with its menus +func (s *Systray) Start() { + if s.Hibernate { + systray.Run(s.startHibernate, s.end) + } else { + systray.Run(s.start, s.end) + } +} + +// Quit simply exits the program +func (s *Systray) Quit() { + systray.Quit() +} + +// start creates a systray icon with menu options to go to arduino create, open debug, pause/quit the agent +func (s *Systray) start() { + systray.SetIcon(icon.GetIcon()) + + // Add version + menuVer := systray.AddMenuItem("Agent version "+s.Version, "") + menuVer.Disable() + + // Add links + mUrl := systray.AddMenuItem("Go to Arduino Create", "Arduino Create") + mDebug := systray.AddMenuItem("Open Debug Console", "Debug console") + + // Add pause/quit + mPause := systray.AddMenuItem("Pause Plugin", "") + systray.AddSeparator() + mQuit := systray.AddMenuItem("Quit Plugin", "") + + // Add configs + s.addConfigs() + + // listen for events + go func() { + for { + select { + case <-mUrl.ClickedCh: + _ = open.Start("https://create.arduino.cc") + case <-mDebug.ClickedCh: + _ = open.Start(s.DebugURL()) + case <-mPause.ClickedCh: + s.Pause() + case <-mQuit.ClickedCh: + s.Quit() + } + } + }() +} + +// starthibernate creates a systray icon with menu options to resume/quit the agent +func (s *Systray) startHibernate() { + systray.SetIcon(icon.GetIconHiber()) + + mResume := systray.AddMenuItem("Resume Plugin", "") + systray.AddSeparator() + mQuit := systray.AddMenuItem("Quit Plugin", "") + + // listen for events + go func() { + for { + select { + case <-mResume.ClickedCh: + s.Resume() + case <-mQuit.ClickedCh: + s.Quit() + } + } + }() +} + +// end simply exits the program +func (s *Systray) end() { + os.Exit(0) +} + +func (s *Systray) addConfigs() { + var mConfigCheckbox []*systray.MenuItem + + configs := getConfigs() + if len(configs) > 1 { + for _, config := range configs { + entry := systray.AddMenuItem(config.Name, "") + mConfigCheckbox = append(mConfigCheckbox, entry) + // decorate configs + gliph := " ☐ " + if s.AdditionalConfig == config.Location { + gliph = " 🗹 " + } + entry.SetTitle(gliph + config.Name) + } + } + + // It would be great to use the select channel here, + // but unfortunately there's no clean way to do it with an array of channels, so we start a single goroutine for each of them + for i := range mConfigCheckbox { + go func(v int) { + <-mConfigCheckbox[v].ClickedCh + s.AdditionalConfig = configs[v].Location + s.Restart() + }(i) + } +} + +type configIni struct { + Name string + Location string +} + +// getconfigs parses all config files in the executable folder +func getConfigs() []configIni { + // config.ini must be there, so call it Default + src, _ := osext.Executable() + dest := filepath.Dir(src) + + var configs []configIni + + err := filepath.Walk(dest, func(path string, f os.FileInfo, _ error) error { + if !f.IsDir() { + if filepath.Ext(path) == ".ini" { + cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, filepath.Join(dest, f.Name())) + if err != nil { + return err + } + defaultSection, err := cfg.GetSection("") + name := defaultSection.Key("name").String() + if name == "" || err != nil { + name = "Default config" + } + conf := configIni{Name: name, Location: f.Name()} + configs = append(configs, conf) + } + } + return nil + }) + + if err != nil { + fmt.Println("error walking through executable configuration: %w", err) + } + + return configs +} diff --git a/trayicon.go b/trayicon.go deleted file mode 100644 index 87773633f..000000000 --- a/trayicon.go +++ /dev/null @@ -1,202 +0,0 @@ -// -// trayicon.go -// -// Created by Martino Facchin -// Copyright (c) 2015 Arduino LLC -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -// +build !cli - -package main - -import ( - "os" - "path/filepath" - "runtime" - - "github.com/arduino/arduino-create-agent/icon" - "github.com/getlantern/systray" - "github.com/go-ini/ini" - "github.com/kardianos/osext" - log "github.com/sirupsen/logrus" - "github.com/skratchdot/open-golang/open" - "go.bug.st/serial.v1" -) - -func setupSysTray() { - runtime.LockOSThread() - if *hibernate == true { - systray.Run(setupSysTrayHibernate, nil) - } else { - systray.Run(setupSysTrayReal, nil) - } -} - -func addRebootTrayElement() { - reboot_tray := systray.AddMenuItem("Reboot to update", "") - - go func() { - <-reboot_tray.ClickedCh - systray.Quit() - log.Println("Restarting now...") - log.Println("Restart because addReebotTrayElement") - restart("") - }() -} - -type ConfigIni struct { - Name string - Localtion string -} - -func getConfigs() []ConfigIni { - // parse all configs in executable folder - // config.ini must be there, so call it Default - src, _ := osext.Executable() - dest := filepath.Dir(src) - - var configs []ConfigIni - - filepath.Walk(dest, func(path string, f os.FileInfo, _ error) error { - if !f.IsDir() { - if filepath.Ext(path) == ".ini" { - cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, filepath.Join(dest, f.Name())) - if err != nil { - return err - } - defaultSection, err := cfg.GetSection("") - name := defaultSection.Key("name").String() - if name == "" || err != nil { - name = "Default config" - } - conf := ConfigIni{Name: name, Localtion: f.Name()} - configs = append(configs, conf) - } - } - return nil - }) - return configs -} - -func setupSysTrayReal() { - - systray.SetIcon(icon.GetIcon()) - menuVer := systray.AddMenuItem("Agent version "+version+"-"+git_revision, "") - systray.AddSeparator() - mUrl := systray.AddMenuItem("Go to Arduino Create", "Arduino Create") - mDebug := systray.AddMenuItem("Open Debug Console", "Debug console") - mPause := systray.AddMenuItem("Pause Plugin", "") - systray.AddSeparator() - mQuit := systray.AddMenuItem("Quit Plugin", "") - - var mConfigCheckbox []*systray.MenuItem - - configs := getConfigs() - - if len(configs) > 1 { - for _, config := range configs { - entry := systray.AddMenuItem(config.Name, "") - mConfigCheckbox = append(mConfigCheckbox, entry) - // decorate configs - gliph := " ☐ " - if *additionalConfig == config.Localtion { - gliph = " 🗹 " - } - entry.SetTitle(gliph + config.Name) - } - } - //mQuit := systray.AddMenuItem("Quit Plugin", "") - - menuVer.Disable() - - for i, _ := range mConfigCheckbox { - go func(v int) { - for { - <-mConfigCheckbox[v].ClickedCh - - restart("", "-additional-config", configs[v].Localtion) - } - }(i) - } - - go func() { - <-mPause.ClickedCh - ports, _ := serial.GetPortsList() - for _, element := range ports { - spClose(element) - } - systray.Quit() - *hibernate = true - log.Println("Restart because setup went wrong?") - restart("") - }() - - go func() { - <-mQuit.ClickedCh - systray.Quit() - exit() - }() - - go func() { - for { - <-mDebug.ClickedCh - logAction("log on") - open.Start("http://127.0.0.1" + port) - } - }() - - // We can manipulate the systray in other goroutines - go func() { - for { - <-mUrl.ClickedCh - open.Start("https://create.arduino.cc") - } - }() -} - -func setupSysTrayHibernate() { - - systray.SetIcon(icon.GetIconHiber()) - mOpen := systray.AddMenuItem("Resume Plugin", "") - systray.AddSeparator() - mQuit := systray.AddMenuItem("Quit Plugin", "") - - go func() { - <-mOpen.ClickedCh - *hibernate = false - log.Println("Restart for hibernation") - systray.Quit() - restart("") - }() - - go func() { - <-mQuit.ClickedCh - systray.Quit() - exit() - }() -} - -func quitSysTray() { - systray.Quit() -} diff --git a/trayicon_fake.go b/trayicon_fake.go deleted file mode 100644 index e525813dc..000000000 --- a/trayicon_fake.go +++ /dev/null @@ -1,43 +0,0 @@ -// -// trayicon.go -// -// Created by Martino Facchin -// Copyright (c) 2015 Arduino LLC -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -// +build cli - -package main - -func setupSysTray() { - //no systray support for arm yet - select {} -} - -func addRebootTrayElement() { - select {} -} - -func quitSysTray() { -} diff --git a/update.go b/update.go index bb219212d..f8a17a4ac 100644 --- a/update.go +++ b/update.go @@ -61,5 +61,5 @@ func updateHandler(c *gin.Context) { } c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) - go restart(path) + Systray.Update(path) }