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