Skip to content

Commit f973044

Browse files
authored
Merge pull request #544 from arduino/refactor_systray
Refactor systray
2 parents dda98a2 + d9c50f7 commit f973044

File tree

9 files changed

+268
-304
lines changed

9 files changed

+268
-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

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

systray/systray_fake.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// +build cli
2+
3+
// Systray_fake gets compiled when the tag 'cli' is present. This is useful to build an agent without trayicon functionalities
4+
package systray
5+
6+
import "os"
7+
8+
func (s *Systray) Start() {
9+
select {}
10+
}
11+
12+
func (s *Systray) Quit() {
13+
os.Exit(0)
14+
}

systray/systray_real.go

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

0 commit comments

Comments
 (0)