Skip to content

Autostart is handled by the agent itself #781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 5, 2023
16 changes: 16 additions & 0 deletions config/ArduinoCreateAgent.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<false/>
<key>Label</key>
<string>cc.arduino.arduino-create-agent</string>
<key>Program</key>
<string>{{.Program}}</string>
<key>RunAtLoad</key>
<{{.RunAtLoad}}/>
<key>AbandonProcessGroup</key>
<true/>
</dict>
</plist>
124 changes: 124 additions & 0 deletions config/autostart.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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
}
16 changes: 16 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
crashreport = false # enable crashreport logging
autostartMacOS = true # the Arduino Create Agent is able to start automatically after login on macOS (launchd agent)
40 changes: 25 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down