Skip to content

Commit 7f31d0e

Browse files
authored
Autostart is handled by the agent itself (#781)
* [test] leverage launchd tool to implement autostart feature on macos * use embed and template * add check on macos * exit after loading the agent, `launchctl load ...` will start the binary * test new version of the installer config * change the Label in the plist file to comply with the apple convention * factor out the logic of Install/Uninstall of the plist in config package * made functions private * Revert "test new version of the installer config" This reverts commit ff33dbc. * fix lint
1 parent 71e4921 commit 7f31d0e

File tree

5 files changed

+183
-16
lines changed

5 files changed

+183
-16
lines changed

config/ArduinoCreateAgent.plist

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>KeepAlive</key>
6+
<false/>
7+
<key>Label</key>
8+
<string>cc.arduino.arduino-create-agent</string>
9+
<key>Program</key>
10+
<string>{{.Program}}</string>
11+
<key>RunAtLoad</key>
12+
<{{.RunAtLoad}}/>
13+
<key>AbandonProcessGroup</key>
14+
<true/>
15+
</dict>
16+
</plist>

config/autostart.go

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// This program is free software: you can redistribute it and/or modify
3+
// it under the terms of the GNU Affero General Public License as published
4+
// by the Free Software Foundation, either version 3 of the License, or
5+
// (at your option) any later version.
6+
//
7+
// This program is distributed in the hope that it will be useful,
8+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
// GNU Affero General Public License for more details.
11+
//
12+
// You should have received a copy of the GNU Affero General Public License
13+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
14+
15+
package config
16+
17+
import (
18+
// we need this for the ArduinoCreateAgent.plist in this package
19+
_ "embed"
20+
"os"
21+
"os/exec"
22+
"text/template"
23+
24+
"github.com/arduino/go-paths-helper"
25+
log "github.com/sirupsen/logrus"
26+
)
27+
28+
//go:embed ArduinoCreateAgent.plist
29+
var launchdAgentDefinition []byte
30+
31+
// getLaunchdAgentPath will return the path of the launchd agent default path
32+
func getLaunchdAgentPath() *paths.Path {
33+
return GetDefaultHomeDir().Join("Library", "LaunchAgents", "ArduinoCreateAgent.plist")
34+
}
35+
36+
// InstallPlistFile will handle the process of creating the plist file required for the autostart
37+
// and loading it using launchd
38+
func InstallPlistFile() {
39+
launchdAgentPath := getLaunchdAgentPath()
40+
if !launchdAgentPath.Exist() {
41+
err := writePlistFile(launchdAgentPath)
42+
if err != nil {
43+
log.Error(err)
44+
} else {
45+
err = loadLaunchdAgent() // this will load the agent: basically starting a new instance
46+
if err != nil {
47+
log.Error(err)
48+
} else {
49+
log.Info("Quitting, another instance of the agent has been started by launchd")
50+
os.Exit(0)
51+
}
52+
}
53+
} else {
54+
// we already have an existing launchd plist file, so we don't have to do anything
55+
log.Infof("the autostart file %s already exists: nothing to do", launchdAgentPath)
56+
57+
}
58+
}
59+
60+
// writePlistFile function will write the required plist file to launchdAgentPath
61+
// it will return nil in case of success,
62+
// it will error in any other case
63+
func writePlistFile(launchdAgentPath *paths.Path) error {
64+
src, err := os.Executable()
65+
66+
if err != nil {
67+
return err
68+
}
69+
data := struct {
70+
Program string
71+
RunAtLoad bool
72+
}{
73+
Program: src,
74+
RunAtLoad: true, // This will start the agent right after login (and also after `launchctl load ...`)
75+
}
76+
77+
t := template.Must(template.New("launchdConfig").Parse(string(launchdAgentDefinition)))
78+
79+
// we need to create a new launchd plist file
80+
plistFile, _ := launchdAgentPath.Create()
81+
return t.Execute(plistFile, data)
82+
}
83+
84+
// loadLaunchdAgent will use launchctl to load the agent, will return an error if something goes wrong
85+
func loadLaunchdAgent() error {
86+
// https://www.launchd.info/
87+
oscmd := exec.Command("launchctl", "load", getLaunchdAgentPath().String())
88+
err := oscmd.Run()
89+
return err
90+
}
91+
92+
// UninstallPlistFile will handle the process of unloading (unsing launchd) the file required for the autostart
93+
// and removing the file
94+
func UninstallPlistFile() {
95+
err := unloadLaunchdAgent()
96+
if err != nil {
97+
log.Error(err)
98+
} else {
99+
err = removePlistFile()
100+
if err != nil {
101+
log.Error(err)
102+
}
103+
}
104+
}
105+
106+
// unloadLaunchdAgent will use launchctl to load the agent, will return an error if something goes wrong
107+
func unloadLaunchdAgent() error {
108+
// https://www.launchd.info/
109+
oscmd := exec.Command("launchctl", "unload", getLaunchdAgentPath().String())
110+
err := oscmd.Run()
111+
return err
112+
}
113+
114+
// removePlistFile function will remove the plist file from $HOME/Library/LaunchAgents/ArduinoCreateAgent.plist and return an error
115+
// it will not do anything if the file is not there
116+
func removePlistFile() error {
117+
launchdAgentPath := getLaunchdAgentPath()
118+
if launchdAgentPath.Exist() {
119+
log.Infof("removing: %s", launchdAgentPath)
120+
return launchdAgentPath.Remove()
121+
}
122+
log.Infof("the autostart file %s do not exists: nothing to do", launchdAgentPath)
123+
return nil
124+
}

config/config.go

+16
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ func GetDefaultConfigDir() *paths.Path {
9191
return agentConfigDir
9292
}
9393

94+
// GetDefaultHomeDir returns the full path to the user's home directory.
95+
func GetDefaultHomeDir() *paths.Path {
96+
// UserHomeDir returns the current user's home directory.
97+
98+
// On Unix, including macOS, it returns the $HOME environment variable.
99+
// On Windows, it returns %USERPROFILE%.
100+
// On Plan 9, it returns the $home environment variable.
101+
102+
homeDir, err := os.UserHomeDir()
103+
if err != nil {
104+
log.Panicf("Can't get user home dir: %s", err)
105+
}
106+
107+
return paths.New(homeDir)
108+
}
109+
94110
//go:embed config.ini
95111
var configContent []byte
96112

config/config.ini

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ appName = CreateAgent/Stable
66
updateUrl = https://downloads.arduino.cc/
77
origins = https://local.arduino.cc:8000
88
#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
9-
crashreport = false # enable crashreport logging
9+
crashreport = false # enable crashreport logging
10+
autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)

main.go

+25-15
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,22 @@ var (
6666

6767
// iniflags
6868
var (
69-
address = iniConf.String("address", "127.0.0.1", "The address where to listen. Defaults to localhost")
70-
appName = iniConf.String("appName", "", "")
71-
gcType = iniConf.String("gc", "std", "Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)")
72-
hostname = iniConf.String("hostname", "unknown-hostname", "Override the hostname we get from the OS")
73-
httpProxy = iniConf.String("httpProxy", "", "Proxy server for HTTP requests")
74-
httpsProxy = iniConf.String("httpsProxy", "", "Proxy server for HTTPS requests")
75-
indexURL = iniConf.String("indexURL", "https://downloads.arduino.cc/packages/package_staging_index.json", "The address from where to download the index json containing the location of upload tools")
76-
iniConf = flag.NewFlagSet("ini", flag.ContinueOnError)
77-
logDump = iniConf.String("log", "off", "off = (default)")
78-
origins = iniConf.String("origins", "", "Allowed origin list for CORS")
79-
regExpFilter = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list")
80-
signatureKey = iniConf.String("signatureKey", "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF\nIE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1\nZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1\npFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z\nCeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn\n2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9\ntwIDAQAB\n-----END PUBLIC KEY-----", "Pem-encoded public key to verify signed commandlines")
81-
updateURL = iniConf.String("updateUrl", "", "")
82-
verbose = iniConf.Bool("v", true, "show debug logging")
83-
crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
69+
address = iniConf.String("address", "127.0.0.1", "The address where to listen. Defaults to localhost")
70+
appName = iniConf.String("appName", "", "")
71+
gcType = iniConf.String("gc", "std", "Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)")
72+
hostname = iniConf.String("hostname", "unknown-hostname", "Override the hostname we get from the OS")
73+
httpProxy = iniConf.String("httpProxy", "", "Proxy server for HTTP requests")
74+
httpsProxy = iniConf.String("httpsProxy", "", "Proxy server for HTTPS requests")
75+
indexURL = iniConf.String("indexURL", "https://downloads.arduino.cc/packages/package_staging_index.json", "The address from where to download the index json containing the location of upload tools")
76+
iniConf = flag.NewFlagSet("ini", flag.ContinueOnError)
77+
logDump = iniConf.String("log", "off", "off = (default)")
78+
origins = iniConf.String("origins", "", "Allowed origin list for CORS")
79+
regExpFilter = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list")
80+
signatureKey = iniConf.String("signatureKey", "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF\nIE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1\nZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1\npFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z\nCeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn\n2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9\ntwIDAQAB\n-----END PUBLIC KEY-----", "Pem-encoded public key to verify signed commandlines")
81+
updateURL = iniConf.String("updateUrl", "", "")
82+
verbose = iniConf.Bool("v", true, "show debug logging")
83+
crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
84+
autostartMacOS = iniConf.Bool("autostartMacOS", true, "the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)")
8485
)
8586

8687
var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHTML))
@@ -327,6 +328,15 @@ func loop() {
327328
}
328329
}
329330

331+
// macos agent launchd autostart
332+
if runtime.GOOS == "darwin" {
333+
if *autostartMacOS {
334+
config.InstallPlistFile()
335+
} else {
336+
config.UninstallPlistFile()
337+
}
338+
}
339+
330340
// launch the hub routine which is the singleton for the websocket server
331341
go h.run()
332342
// launch our serial port routine

0 commit comments

Comments
 (0)