Skip to content

Refactor systray #544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 2 additions & 54 deletions hub.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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")
}
2 changes: 1 addition & 1 deletion info.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func pauseHandler(c *gin.Context) {
spClose(element)
}
*hibernate = true
restart("")
Systray.Pause()
}()
c.JSON(200, nil)
}
17 changes: 14 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -67,7 +68,8 @@ var (

// global clients
var (
Tools tools.Tools
Tools tools.Tools
Systray systray.Systray
)

type NullWriter int
Expand Down Expand Up @@ -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() {
Expand Down
76 changes: 76 additions & 0 deletions systray/systray.go
Original file line number Diff line number Diff line change
@@ -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()
}
14 changes: 14 additions & 0 deletions systray/systray_fake.go
Original file line number Diff line number Diff line change
@@ -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)
}
160 changes: 160 additions & 0 deletions systray/systray_real.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading