Skip to content

Commit caffd81

Browse files
committed
Refactor systray
In order to chase a bug which caused the windows version to fail to restart under some circumstances, I encapsulated the code regarding the trayicon in a package. The new version should be easier to mantain Signed-off-by: Matteo Suppo <[email protected]>
1 parent dda98a2 commit caffd81

File tree

9 files changed

+265
-304
lines changed

9 files changed

+265
-304
lines changed

hub.go

+2-54
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
package main
22

33
import (
4-
"fmt"
5-
64
"encoding/json"
75
"io"
86
"os"
9-
"os/exec"
107
"runtime"
118
"runtime/debug"
129
"strconv"
1310
"strings"
1411

1512
"github.com/arduino/arduino-create-agent/upload"
16-
"github.com/kardianos/osext"
1713
log "github.com/sirupsen/logrus"
1814
)
1915

@@ -210,9 +206,9 @@ func checkCmd(m []byte) {
210206
go logAction(sl)
211207
} else if strings.HasPrefix(sl, "restart") {
212208
log.Println("Received restart from the daemon. Why? Boh")
213-
restart("")
209+
Systray.Restart()
214210
} else if strings.HasPrefix(sl, "exit") {
215-
exit()
211+
Systray.Quit()
216212
} else if strings.HasPrefix(sl, "memstats") {
217213
memoryStats()
218214
} else if strings.HasPrefix(sl, "gc") {
@@ -267,51 +263,3 @@ func garbageCollection() {
267263
h.broadcastSys <- []byte("{\"gc\":\"done\"}")
268264
memoryStats()
269265
}
270-
271-
func exit() {
272-
quitSysTray()
273-
log.Println("Starting new spjs process")
274-
h.broadcastSys <- []byte("{\"Exiting\" : true}")
275-
log.Fatal("Exited current spjs cuz asked to")
276-
277-
}
278-
279-
func restart(path string, args ...string) {
280-
log.Println("called restart", path)
281-
quitSysTray()
282-
// relaunch ourself and exit
283-
// the relaunch works because we pass a cmdline in
284-
// that has serial-port-json-server only initialize 5 seconds later
285-
// which gives us time to exit and unbind from serial ports and TCP/IP
286-
// sockets like :8989
287-
log.Println("Starting new spjs process")
288-
h.broadcastSys <- []byte("{\"Restarting\" : true}")
289-
290-
// figure out current path of executable so we know how to restart
291-
// this process using osext
292-
exePath, err3 := osext.Executable()
293-
if err3 != nil {
294-
log.Printf("Error getting exe path using osext lib. err: %v\n", err3)
295-
}
296-
297-
if path == "" {
298-
log.Printf("exePath using osext: %v\n", exePath)
299-
} else {
300-
exePath = path
301-
}
302-
303-
exePath = strings.Trim(exePath, "\n")
304-
305-
args = append(args, "-ls")
306-
args = append(args, "-hibernate="+fmt.Sprint(*hibernate))
307-
cmd := exec.Command(exePath, args...)
308-
309-
err := cmd.Start()
310-
if err != nil {
311-
log.Printf("Got err restarting spjs: %v\n", err)
312-
h.broadcastSys <- []byte("{\"Error\" : \"" + fmt.Sprintf("%v", err) + "\"}")
313-
} else {
314-
h.broadcastSys <- []byte("{\"Restarted\" : true}")
315-
}
316-
log.Fatal("Exited current spjs for restart")
317-
}

info.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func pauseHandler(c *gin.Context) {
3232
spClose(element)
3333
}
3434
*hibernate = true
35-
restart("")
35+
Systray.Pause()
3636
}()
3737
c.JSON(200, nil)
3838
}

main.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515
"text/template"
1616
"time"
1717

18+
"github.com/arduino/arduino-create-agent/systray"
1819
"github.com/arduino/arduino-create-agent/tools"
1920
"github.com/arduino/arduino-create-agent/utilities"
20-
"github.com/arduino/arduino-create-agent/v2"
21+
v2 "github.com/arduino/arduino-create-agent/v2"
2122
"github.com/gin-gonic/gin"
2223
"github.com/go-ini/ini"
2324
cors "github.com/itsjamie/gin-cors"
@@ -67,7 +68,8 @@ var (
6768

6869
// global clients
6970
var (
70-
Tools tools.Tools
71+
Tools tools.Tools
72+
Systray systray.Systray
7173
)
7274

7375
type NullWriter int
@@ -107,7 +109,16 @@ func main() {
107109
go loop()
108110

109111
// SetupSystray is the main thread
110-
setupSysTray()
112+
Systray = systray.Systray{
113+
Hibernate: *hibernate,
114+
Version: version + "-" + git_revision,
115+
DebugURL: func() string {
116+
return "http://" + *address + port
117+
},
118+
AdditionalConfig: *additionalConfig,
119+
}
120+
121+
Systray.Start()
111122
}
112123

113124
func loop() {

systray/systray.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package systray
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/kardianos/osext"
9+
)
10+
11+
type Systray struct {
12+
// Whether the Agent is in Pause mode
13+
Hibernate bool
14+
// The version of the Agent, displayed in the trayicon menu
15+
Version string
16+
// The url of the debug page. It's a function because it could change port
17+
DebugURL func() string
18+
// The active configuration file
19+
AdditionalConfig string
20+
// The path of the exe (only used in update)
21+
path string
22+
}
23+
24+
// Restart restarts the program
25+
// it works by finding the executable path and launching it before quitting
26+
func (s *Systray) Restart() {
27+
if s.path == "" {
28+
var err error
29+
s.path, err = osext.Executable()
30+
if err != nil {
31+
fmt.Printf("Error getting exe path using osext lib. err: %v\n", err)
32+
}
33+
34+
// Trim newlines (needed on osx)
35+
s.path = strings.Trim(s.path, "\n")
36+
}
37+
38+
// Build args
39+
args := []string{"-ls", fmt.Sprintf("--hibernate=%v", s.Hibernate)}
40+
41+
if s.AdditionalConfig != "" {
42+
args = append(args, fmt.Sprintf("--additional-config=%s", s.AdditionalConfig))
43+
}
44+
45+
fmt.Println(s.path, args)
46+
47+
// Launch executable
48+
cmd := exec.Command(s.path, args...)
49+
err := cmd.Start()
50+
if err != nil {
51+
fmt.Printf("Error restarting process: %v\n", err)
52+
return
53+
}
54+
55+
// If everything was fine, quit
56+
s.Quit()
57+
}
58+
59+
// Pause restarts the program with the hibernate flag set to true
60+
func (s *Systray) Pause() {
61+
s.Hibernate = true
62+
s.Restart()
63+
}
64+
65+
// Pause restarts the program with the hibernate flag set to false
66+
func (s *Systray) Resume() {
67+
s.Hibernate = false
68+
s.Restart()
69+
}
70+
71+
// Update restarts the program with the given path
72+
func (s *Systray) Update(path string) {
73+
s.path = path
74+
s.Restart()
75+
}

systray/systray_fake.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// +build cli
2+
3+
package systray
4+
5+
import "os"
6+
7+
func (s *Systray) Start() {
8+
select {}
9+
}
10+
11+
func (s *Systray) Quit() {
12+
os.Exit(0)
13+
}

systray/systray_real.go

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// +build !cli
2+
3+
package systray
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/arduino/arduino-create-agent/icon"
11+
"github.com/getlantern/systray"
12+
"github.com/go-ini/ini"
13+
"github.com/kardianos/osext"
14+
"github.com/skratchdot/open-golang/open"
15+
)
16+
17+
// Start sets up the systray icon with its menus
18+
func (s *Systray) Start() {
19+
if s.Hibernate {
20+
systray.Run(s.startHibernate, s.end)
21+
} else {
22+
systray.Run(s.start, s.end)
23+
}
24+
}
25+
26+
// Quit simply exits the program
27+
func (s *Systray) Quit() {
28+
systray.Quit()
29+
}
30+
31+
// start creates a systray icon with menu options to go to arduino create, open debug, pause/quit the agent
32+
func (s *Systray) start() {
33+
systray.SetIcon(icon.GetIcon())
34+
35+
// Add version
36+
menuVer := systray.AddMenuItem("Agent version "+s.Version, "")
37+
menuVer.Disable()
38+
39+
// Add links
40+
mUrl := systray.AddMenuItem("Go to Arduino Create", "Arduino Create")
41+
mDebug := systray.AddMenuItem("Open Debug Console", "Debug console")
42+
43+
// Add pause/quit
44+
mPause := systray.AddMenuItem("Pause Plugin", "")
45+
systray.AddSeparator()
46+
mQuit := systray.AddMenuItem("Quit Plugin", "")
47+
48+
// Add configs
49+
s.addConfigs()
50+
51+
// listen for events
52+
go func() {
53+
for {
54+
select {
55+
case <-mUrl.ClickedCh:
56+
_ = open.Start("https://create.arduino.cc")
57+
case <-mDebug.ClickedCh:
58+
_ = open.Start(s.DebugURL())
59+
case <-mPause.ClickedCh:
60+
s.Pause()
61+
case <-mQuit.ClickedCh:
62+
s.Quit()
63+
}
64+
}
65+
}()
66+
}
67+
68+
// starthibernate creates a systray icon with menu options to resume/quit the agent
69+
func (s *Systray) startHibernate() {
70+
systray.SetIcon(icon.GetIconHiber())
71+
72+
mResume := systray.AddMenuItem("Resume Plugin", "")
73+
systray.AddSeparator()
74+
mQuit := systray.AddMenuItem("Quit Plugin", "")
75+
76+
// listen for events
77+
go func() {
78+
for {
79+
select {
80+
case <-mResume.ClickedCh:
81+
s.Resume()
82+
case <-mQuit.ClickedCh:
83+
s.Quit()
84+
}
85+
}
86+
}()
87+
}
88+
89+
// end simply exits the program
90+
func (s *Systray) end() {
91+
os.Exit(0)
92+
}
93+
94+
func (s *Systray) addConfigs() {
95+
var mConfigCheckbox []*systray.MenuItem
96+
97+
configs := getConfigs()
98+
if len(configs) > 1 {
99+
for _, config := range configs {
100+
entry := systray.AddMenuItem(config.Name, "")
101+
mConfigCheckbox = append(mConfigCheckbox, entry)
102+
// decorate configs
103+
gliph := " ☐ "
104+
if s.AdditionalConfig == config.Location {
105+
gliph = " 🗹 "
106+
}
107+
entry.SetTitle(gliph + config.Name)
108+
}
109+
}
110+
111+
// It would be great to use the select channel here,
112+
// 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
113+
for i := range mConfigCheckbox {
114+
go func(v int) {
115+
<-mConfigCheckbox[v].ClickedCh
116+
s.AdditionalConfig = configs[v].Location
117+
s.Restart()
118+
}(i)
119+
}
120+
}
121+
122+
type configIni struct {
123+
Name string
124+
Location string
125+
}
126+
127+
// getconfigs parses all config files in the executable folder
128+
func getConfigs() []configIni {
129+
// config.ini must be there, so call it Default
130+
src, _ := osext.Executable()
131+
dest := filepath.Dir(src)
132+
133+
var configs []configIni
134+
135+
err := filepath.Walk(dest, func(path string, f os.FileInfo, _ error) error {
136+
if !f.IsDir() {
137+
if filepath.Ext(path) == ".ini" {
138+
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, filepath.Join(dest, f.Name()))
139+
if err != nil {
140+
return err
141+
}
142+
defaultSection, err := cfg.GetSection("")
143+
name := defaultSection.Key("name").String()
144+
if name == "" || err != nil {
145+
name = "Default config"
146+
}
147+
conf := configIni{Name: name, Location: f.Name()}
148+
configs = append(configs, conf)
149+
}
150+
}
151+
return nil
152+
})
153+
154+
if err != nil {
155+
fmt.Println("error walking through executable configuration: %w", err)
156+
}
157+
158+
return configs
159+
}

0 commit comments

Comments
 (0)