-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement darwin launch agent (#192)
* feat: sketch out periodic command * feat: sketch out periodic command for macOS * feat: implement darwin's launch agent * refactor: better way to run on darwin Make sure we have code that builds on all platforms. * fix(run): max 10 URLs with darwin in unattended mode * feat: add support for seeing/streaming logs * feat: implement the status command and add usage hints * feat(periodic): run onboarding if needed * fix: no too confusing function names * fix: s/periodic/autorun/ Discussed earlier this morning with @hellais. * fix: we cannot show logs before Big Sur Bug reported by @hellais.
- Loading branch information
1 parent
6fbad85
commit 504a4e7
Showing
7 changed files
with
334 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// Package autorun contains code to manage automatic runs | ||
package autorun | ||
|
||
import "sync" | ||
|
||
const ( | ||
// StatusScheduled indicates that OONI is scheduled to run | ||
// periodically in the background. | ||
StatusScheduled = "scheduled" | ||
|
||
// StatusStopped indicates that OONI is not scheduled to | ||
// run periodically in the background. | ||
StatusStopped = "stopped" | ||
|
||
// StatusRunning indicates that OONI is currently | ||
// running in the background. | ||
StatusRunning = "running" | ||
) | ||
|
||
// Manager manages automatic runs | ||
type Manager interface { | ||
LogShow() error | ||
LogStream() error | ||
Start() error | ||
Status() (string, error) | ||
Stop() error | ||
} | ||
|
||
var ( | ||
registry map[string]Manager | ||
mtx sync.Mutex | ||
) | ||
|
||
func register(platform string, manager Manager) { | ||
defer mtx.Unlock() | ||
mtx.Lock() | ||
if registry == nil { | ||
registry = make(map[string]Manager) | ||
} | ||
registry[platform] = manager | ||
} | ||
|
||
// Get gets the specified autorun manager. This function | ||
// returns nil if no autorun manager exists. | ||
func Get(platform string) Manager { | ||
defer mtx.Unlock() | ||
mtx.Lock() | ||
return registry[platform] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
package autorun | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"os/exec" | ||
"strconv" | ||
"strings" | ||
"text/template" | ||
|
||
"github.com/apex/log" | ||
"github.com/ooni/probe-cli/internal/utils" | ||
"github.com/ooni/probe-engine/cmd/jafar/shellx" | ||
"golang.org/x/sys/unix" | ||
) | ||
|
||
type managerDarwin struct{} | ||
|
||
var ( | ||
plistPath = os.ExpandEnv("$HOME/Library/LaunchAgents/org.ooni.cli.plist") | ||
domainTarget = fmt.Sprintf("gui/%d", os.Getuid()) | ||
serviceTarget = fmt.Sprintf("%s/org.ooni.cli", domainTarget) | ||
) | ||
|
||
var plistTemplate = ` | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>Label</key> | ||
<string>org.ooni.cli</string> | ||
<key>KeepAlive</key> | ||
<false/> | ||
<key>RunAtLoad</key> | ||
<true/> | ||
<key>ProgramArguments</key> | ||
<array> | ||
<string>{{ .Executable }}</string> | ||
<string>--log-handler=syslog</string> | ||
<string>run</string> | ||
<string>unattended</string> | ||
</array> | ||
<key>StartInterval</key> | ||
<integer>3600</integer> | ||
</dict> | ||
</plist> | ||
` | ||
|
||
func runQuiteQuietly(name string, arg ...string) error { | ||
log.Infof("exec: %s %s", name, strings.Join(arg, " ")) | ||
return shellx.RunQuiet(name, arg...) | ||
} | ||
|
||
func darwinVersionMajor() (int, error) { | ||
out, err := exec.Command("uname", "-r").Output() | ||
if err != nil { | ||
return 0, err | ||
} | ||
v := bytes.Split(out, []byte(".")) | ||
if len(v) != 3 { | ||
return 0, errors.New("cannot split version") | ||
} | ||
major, err := strconv.Atoi(string(v[0])) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return major, nil | ||
} | ||
|
||
var errNotImplemented = errors.New( | ||
"autorun: command not implemented in this version of macOS") | ||
|
||
func (managerDarwin) LogShow() error { | ||
major, _ := darwinVersionMajor() | ||
if major < 20 /* macOS 11.0 Big Sur */ { | ||
return errNotImplemented | ||
} | ||
return shellx.Run("log", "show", "--info", "--debug", | ||
"--process", "ooniprobe", "--style", "compact") | ||
} | ||
|
||
func (managerDarwin) LogStream() error { | ||
return shellx.Run("log", "stream", "--style", "compact", "--level", | ||
"debug", "--process", "ooniprobe") | ||
} | ||
|
||
func (managerDarwin) mustNotHavePlist() error { | ||
log.Infof("exec: test -f %s && already_registered()", plistPath) | ||
if utils.FileExists(plistPath) { | ||
// This is not atomic. Do we need atomicity here? | ||
return errors.New("autorun: service already registered") | ||
} | ||
return nil | ||
} | ||
|
||
func (managerDarwin) writePlist() error { | ||
executable, err := os.Executable() | ||
if err != nil { | ||
return err | ||
} | ||
var out bytes.Buffer | ||
t := template.Must(template.New("plist").Parse(plistTemplate)) | ||
in := struct{ Executable string }{Executable: executable} | ||
if err := t.Execute(&out, in); err != nil { | ||
return err | ||
} | ||
log.Infof("exec: writePlist(%s)", plistPath) | ||
return ioutil.WriteFile(plistPath, out.Bytes(), 0644) | ||
} | ||
|
||
func (managerDarwin) start() error { | ||
if err := runQuiteQuietly("launchctl", "enable", serviceTarget); err != nil { | ||
return err | ||
} | ||
return runQuiteQuietly("launchctl", "bootstrap", domainTarget, plistPath) | ||
} | ||
|
||
func (m managerDarwin) Start() error { | ||
operations := []func() error{m.mustNotHavePlist, m.writePlist, m.start} | ||
for _, op := range operations { | ||
if err := op(); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (managerDarwin) stop() error { | ||
var failure *exec.ExitError | ||
err := runQuiteQuietly("launchctl", "bootout", serviceTarget) | ||
if errors.As(err, &failure) && failure.ExitCode() == int(unix.ESRCH) { | ||
err = nil | ||
} | ||
return err | ||
} | ||
|
||
func (managerDarwin) removeFile() error { | ||
log.Infof("exec: rm -f %s", plistPath) | ||
err := os.Remove(plistPath) | ||
if errors.Is(err, unix.ENOENT) { | ||
err = nil | ||
} | ||
return err | ||
} | ||
|
||
func (m managerDarwin) Stop() error { | ||
operations := []func() error{m.stop, m.removeFile} | ||
for _, op := range operations { | ||
if err := op(); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (m managerDarwin) Status() (string, error) { | ||
err := runQuiteQuietly("launchctl", "kill", "SIGINFO", serviceTarget) | ||
var failure *exec.ExitError | ||
if errors.As(err, &failure) { | ||
switch failure.ExitCode() { | ||
case int(unix.ESRCH): | ||
return StatusScheduled, nil | ||
case 113: // exit code when there's no plist | ||
return StatusStopped, nil | ||
} | ||
} | ||
if err != nil { | ||
return "", fmt.Errorf("autorun: unexpected error: %w", err) | ||
} | ||
return StatusRunning, nil | ||
} | ||
|
||
func init() { | ||
register("darwin", managerDarwin{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package autorun | ||
|
||
import ( | ||
"errors" | ||
"runtime" | ||
|
||
"github.com/alecthomas/kingpin" | ||
"github.com/apex/log" | ||
"github.com/ooni/probe-cli/internal/autorun" | ||
"github.com/ooni/probe-cli/internal/cli/onboard" | ||
"github.com/ooni/probe-cli/internal/cli/root" | ||
) | ||
|
||
var errNotImplemented = errors.New("autorun: not implemented on this platform") | ||
|
||
func init() { | ||
cmd := root.Command("autorun", "Run automatic tests in the background") | ||
cmd.Action(func(_ *kingpin.ParseContext) error { | ||
probe, err := root.Init() | ||
if err != nil { | ||
log.Errorf("%s", err) | ||
return err | ||
} | ||
if err := onboard.MaybeOnboarding(probe); err != nil { | ||
log.WithError(err).Error("failed to perform onboarding") | ||
return err | ||
} | ||
return nil | ||
}) | ||
|
||
start := cmd.Command("start", "Start running automatic tests in the background") | ||
start.Action(func(_ *kingpin.ParseContext) error { | ||
svc := autorun.Get(runtime.GOOS) | ||
if svc == nil { | ||
return errNotImplemented | ||
} | ||
if err := svc.Start(); err != nil { | ||
return err | ||
} | ||
log.Info("hint: use 'ooniprobe autorun log stream' to follow logs") | ||
return nil | ||
}) | ||
|
||
stop := cmd.Command("stop", "Stop running automatic tests in the background") | ||
stop.Action(func(_ *kingpin.ParseContext) error { | ||
svc := autorun.Get(runtime.GOOS) | ||
if svc == nil { | ||
return errNotImplemented | ||
} | ||
return svc.Stop() | ||
}) | ||
|
||
logCmd := cmd.Command("log", "Access background runs logs") | ||
stream := logCmd.Command("stream", "Stream background runs logs") | ||
stream.Action(func(_ *kingpin.ParseContext) error { | ||
svc := autorun.Get(runtime.GOOS) | ||
if svc == nil { | ||
return errNotImplemented | ||
} | ||
return svc.LogStream() | ||
}) | ||
|
||
show := logCmd.Command("show", "Show background runs logs") | ||
show.Action(func(_ *kingpin.ParseContext) error { | ||
svc := autorun.Get(runtime.GOOS) | ||
if svc == nil { | ||
return errNotImplemented | ||
} | ||
return svc.LogShow() | ||
}) | ||
|
||
status := cmd.Command("status", "Shows autorun instance status") | ||
status.Action(func(_ *kingpin.ParseContext) error { | ||
svc := autorun.Get(runtime.GOOS) | ||
if svc == nil { | ||
return errNotImplemented | ||
} | ||
out, err := svc.Status() | ||
if err != nil { | ||
return err | ||
} | ||
log.Infof("status: %s", out) | ||
switch out { | ||
case autorun.StatusRunning: | ||
log.Info("hint: use 'ooniprobe autorun stop' to stop") | ||
log.Info("hint: use 'ooniprobe autorun log stream' to follow logs") | ||
case autorun.StatusScheduled: | ||
log.Info("hint: use 'ooniprobe autorun stop' to stop") | ||
log.Info("hint: use 'ooniprobe autorun log show' to see previous logs") | ||
case autorun.StatusStopped: | ||
log.Info("hint: use 'ooniprobe autorun start' to start") | ||
} | ||
return nil | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters