diff --git a/config/ArduinoCreateAgent.plist b/config/ArduinoCreateAgent.plist
new file mode 100644
index 000000000..7e85cc3b5
--- /dev/null
+++ b/config/ArduinoCreateAgent.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ KeepAlive
+
+ Label
+ cc.arduino.arduino-create-agent
+ Program
+ {{.Program}}
+ RunAtLoad
+ <{{.RunAtLoad}}/>
+ AbandonProcessGroup
+
+
+
\ No newline at end of file
diff --git a/config/autostart.go b/config/autostart.go
new file mode 100644
index 000000000..c6b2b8d52
--- /dev/null
+++ b/config/autostart.go
@@ -0,0 +1,124 @@
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package config
+
+import (
+ // we need this for the ArduinoCreateAgent.plist in this package
+ _ "embed"
+ "os"
+ "os/exec"
+ "text/template"
+
+ "github.com/arduino/go-paths-helper"
+ log "github.com/sirupsen/logrus"
+)
+
+//go:embed ArduinoCreateAgent.plist
+var launchdAgentDefinition []byte
+
+// getLaunchdAgentPath will return the path of the launchd agent default path
+func getLaunchdAgentPath() *paths.Path {
+ return GetDefaultHomeDir().Join("Library", "LaunchAgents", "ArduinoCreateAgent.plist")
+}
+
+// InstallPlistFile will handle the process of creating the plist file required for the autostart
+// and loading it using launchd
+func InstallPlistFile() {
+ launchdAgentPath := getLaunchdAgentPath()
+ if !launchdAgentPath.Exist() {
+ err := writePlistFile(launchdAgentPath)
+ if err != nil {
+ log.Error(err)
+ } else {
+ err = loadLaunchdAgent() // this will load the agent: basically starting a new instance
+ if err != nil {
+ log.Error(err)
+ } else {
+ log.Info("Quitting, another instance of the agent has been started by launchd")
+ os.Exit(0)
+ }
+ }
+ } else {
+ // we already have an existing launchd plist file, so we don't have to do anything
+ log.Infof("the autostart file %s already exists: nothing to do", launchdAgentPath)
+
+ }
+}
+
+// writePlistFile function will write the required plist file to launchdAgentPath
+// it will return nil in case of success,
+// it will error in any other case
+func writePlistFile(launchdAgentPath *paths.Path) error {
+ src, err := os.Executable()
+
+ if err != nil {
+ return err
+ }
+ data := struct {
+ Program string
+ RunAtLoad bool
+ }{
+ Program: src,
+ RunAtLoad: true, // This will start the agent right after login (and also after `launchctl load ...`)
+ }
+
+ t := template.Must(template.New("launchdConfig").Parse(string(launchdAgentDefinition)))
+
+ // we need to create a new launchd plist file
+ plistFile, _ := launchdAgentPath.Create()
+ return t.Execute(plistFile, data)
+}
+
+// loadLaunchdAgent will use launchctl to load the agent, will return an error if something goes wrong
+func loadLaunchdAgent() error {
+ // https://www.launchd.info/
+ oscmd := exec.Command("launchctl", "load", getLaunchdAgentPath().String())
+ err := oscmd.Run()
+ return err
+}
+
+// UninstallPlistFile will handle the process of unloading (unsing launchd) the file required for the autostart
+// and removing the file
+func UninstallPlistFile() {
+ err := unloadLaunchdAgent()
+ if err != nil {
+ log.Error(err)
+ } else {
+ err = removePlistFile()
+ if err != nil {
+ log.Error(err)
+ }
+ }
+}
+
+// unloadLaunchdAgent will use launchctl to load the agent, will return an error if something goes wrong
+func unloadLaunchdAgent() error {
+ // https://www.launchd.info/
+ oscmd := exec.Command("launchctl", "unload", getLaunchdAgentPath().String())
+ err := oscmd.Run()
+ return err
+}
+
+// removePlistFile function will remove the plist file from $HOME/Library/LaunchAgents/ArduinoCreateAgent.plist and return an error
+// it will not do anything if the file is not there
+func removePlistFile() error {
+ launchdAgentPath := getLaunchdAgentPath()
+ if launchdAgentPath.Exist() {
+ log.Infof("removing: %s", launchdAgentPath)
+ return launchdAgentPath.Remove()
+ }
+ log.Infof("the autostart file %s do not exists: nothing to do", launchdAgentPath)
+ return nil
+}
diff --git a/config/config.go b/config/config.go
index 303aadce8..437437e59 100644
--- a/config/config.go
+++ b/config/config.go
@@ -91,6 +91,22 @@ func GetDefaultConfigDir() *paths.Path {
return agentConfigDir
}
+// GetDefaultHomeDir returns the full path to the user's home directory.
+func GetDefaultHomeDir() *paths.Path {
+ // UserHomeDir returns the current user's home directory.
+
+ // On Unix, including macOS, it returns the $HOME environment variable.
+ // On Windows, it returns %USERPROFILE%.
+ // On Plan 9, it returns the $home environment variable.
+
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ log.Panicf("Can't get user home dir: %s", err)
+ }
+
+ return paths.New(homeDir)
+}
+
//go:embed config.ini
var configContent []byte
diff --git a/config/config.ini b/config/config.ini
index 960e5f3d3..f63377db5 100644
--- a/config/config.ini
+++ b/config/config.ini
@@ -6,4 +6,5 @@ appName = CreateAgent/Stable
updateUrl = https://downloads.arduino.cc/
origins = https://local.arduino.cc:8000
#httpProxy = http://your.proxy:port # Proxy server for HTTP requests
-crashreport = false # enable crashreport logging
\ No newline at end of file
+crashreport = false # enable crashreport logging
+autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
\ No newline at end of file
diff --git a/main.go b/main.go
index 1011ccefe..0cb55a325 100755
--- a/main.go
+++ b/main.go
@@ -66,21 +66,22 @@ var (
// iniflags
var (
- address = iniConf.String("address", "127.0.0.1", "The address where to listen. Defaults to localhost")
- appName = iniConf.String("appName", "", "")
- 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)")
- hostname = iniConf.String("hostname", "unknown-hostname", "Override the hostname we get from the OS")
- httpProxy = iniConf.String("httpProxy", "", "Proxy server for HTTP requests")
- httpsProxy = iniConf.String("httpsProxy", "", "Proxy server for HTTPS requests")
- 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")
- iniConf = flag.NewFlagSet("ini", flag.ContinueOnError)
- logDump = iniConf.String("log", "off", "off = (default)")
- origins = iniConf.String("origins", "", "Allowed origin list for CORS")
- regExpFilter = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list")
- 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")
- updateURL = iniConf.String("updateUrl", "", "")
- verbose = iniConf.Bool("v", true, "show debug logging")
- crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
+ address = iniConf.String("address", "127.0.0.1", "The address where to listen. Defaults to localhost")
+ appName = iniConf.String("appName", "", "")
+ 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)")
+ hostname = iniConf.String("hostname", "unknown-hostname", "Override the hostname we get from the OS")
+ httpProxy = iniConf.String("httpProxy", "", "Proxy server for HTTP requests")
+ httpsProxy = iniConf.String("httpsProxy", "", "Proxy server for HTTPS requests")
+ 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")
+ iniConf = flag.NewFlagSet("ini", flag.ContinueOnError)
+ logDump = iniConf.String("log", "off", "off = (default)")
+ origins = iniConf.String("origins", "", "Allowed origin list for CORS")
+ regExpFilter = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list")
+ 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")
+ updateURL = iniConf.String("updateUrl", "", "")
+ verbose = iniConf.Bool("v", true, "show debug logging")
+ crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging")
+ autostartMacOS = iniConf.Bool("autostartMacOS", true, "the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)")
)
var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHTML))
@@ -327,6 +328,15 @@ func loop() {
}
}
+ // macos agent launchd autostart
+ if runtime.GOOS == "darwin" {
+ if *autostartMacOS {
+ config.InstallPlistFile()
+ } else {
+ config.UninstallPlistFile()
+ }
+ }
+
// launch the hub routine which is the singleton for the websocket server
go h.run()
// launch our serial port routine