-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from spiretechnology/cdd/version-2
- Support for Linux, macOS, and Windows - Support for user- and system-level services (startup on user login or system boot) - Log file creation for stderr and stdout - Platform-agnostic data directory management - Pass command line args into services
- Loading branch information
Showing
20 changed files
with
965 additions
and
202 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
name: Pull Request Workflow | ||
on: | ||
pull_request: | ||
workflow_dispatch: | ||
push: | ||
branches: | ||
- master | ||
jobs: | ||
checks: | ||
name: Workspace Checks | ||
runs-on: ubuntu-latest | ||
steps: | ||
|
||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
|
||
- uses: actions/setup-go@v2 | ||
with: | ||
go-version: '1.20' | ||
|
||
- name: Lint | ||
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi | ||
|
||
- name: Tidy | ||
run: | | ||
go mod tidy | ||
if [[ -n $(git status -s) ]]; then exit 1; fi | ||
test: | ||
name: Build and Test | ||
strategy: | ||
matrix: | ||
os: [ubuntu-latest, windows-latest, macos-latest] | ||
runs-on: ${{ matrix.os }} | ||
steps: | ||
|
||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
|
||
- uses: actions/setup-go@v2 | ||
with: | ||
go-version: '1.20' | ||
|
||
- name: Vet | ||
run: go vet ./... | ||
|
||
- name: Test | ||
run: go test ./... |
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,88 @@ | ||
# go-autostart | ||
|
||
Go library to register your app to autostart on startup (supports Linux, macOS, and Windows). | ||
|
||
## Basic example | ||
|
||
```go | ||
// Define your app's autostart behavior | ||
app := autostart.New(autostart.Options{ | ||
Label: "com.mycompany.MyApp", | ||
Vendor: "Company" | ||
Name: "My App", | ||
Description: "My app description", | ||
Mode: autostart.ModeUser, | ||
Arguments: []string{ /* ... */ }, | ||
}) | ||
|
||
// Enable, disable, or check the status | ||
app.Enable() | ||
app.Disable() | ||
app.IsEnabled() | ||
|
||
// To get other useful data | ||
app.DataDir() | ||
app.StdOutPath() | ||
app.StdErrPath() | ||
``` | ||
|
||
## Supported platforms | ||
|
||
- Linux (systemd) | ||
- macOS (launchd) | ||
- Windows (Service Manager) | ||
|
||
## Logging | ||
|
||
When running your process as a service, it's a good idea to make a deliberate decision about where to send logs. | ||
|
||
With `go-autostart`, you can specify file paths for both stdout and stderr. If you don't specify a path, a platform-specific default location will be chosen based on your app details. | ||
|
||
```go | ||
app := autostart.New(autostart.Options{ | ||
// ... | ||
StdoutPath: "/path/to/myapp.log", | ||
StderrPath: "/path/to/myapp.err", | ||
}) | ||
``` | ||
|
||
You can create writers to your custom log files. This will override the global `os.Stdout` and `os.Stderr` with custom writers | ||
that send logs to both the program's stdio and the custom log files. | ||
|
||
Run the following to override the global `os.Stdout` and `os.Stderr` with your custom log files. This function returns a new, wrapped version of stdout. If you still want to write logs to the console, in addition to the log file, writing to this new stdout will do both. | ||
|
||
```go | ||
stdout, err := app.Stdio() | ||
``` | ||
|
||
Then, to write logs to both the console and the log file: | ||
|
||
```go | ||
// Write directly | ||
fmt.Fprintln(stdout, "Hello, world!") | ||
|
||
// Set the output for the `log` package | ||
log.SetOutput(stdout) | ||
log.Println("Hello, world!") | ||
``` | ||
|
||
## Full Example | ||
|
||
```go | ||
// Define your app's autostart behavior | ||
app := autostart.New(autostart.Options{ | ||
Label: "com.mycompany.MyApp", | ||
Vendor: "Company" | ||
Name: "My App", | ||
Description: "My app description", | ||
Mode: autostart.ModeUser, | ||
Arguments: []string{ /* ... */ }, | ||
}) | ||
|
||
// Setup logging to the log files | ||
stdout, err := app.Stdio() | ||
log.SetOutput(stdout) | ||
|
||
// Enable auto-start for the app | ||
app.Enable() | ||
``` |
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,128 @@ | ||
package autostart | ||
|
||
import ( | ||
_ "embed" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"text/template" | ||
|
||
"github.com/spiretechnology/go-autostart/internal/errutil" | ||
) | ||
|
||
//go:embed templates/macos_launchd.plist | ||
var plistTemplate string | ||
|
||
type plistTemplateData struct { | ||
Options Options | ||
BinaryPath string | ||
EscapedArguments []string | ||
} | ||
|
||
func (a *autostart) IsEnabled() (bool, error) { | ||
plistPath, err := a.getPlistFilePath() | ||
if err != nil { | ||
return false, errutil.Wrapf(err, "getting plist file path") | ||
} | ||
if _, err = os.Stat(plistPath); err != nil { | ||
if os.IsNotExist(err) { | ||
return false, nil | ||
} | ||
return false, errutil.Wrapf(err, "checking if plist file exists") | ||
} | ||
return true, nil | ||
} | ||
|
||
func (a *autostart) Enable() error { | ||
// Get the path to the binary file | ||
binaryPath, err := os.Executable() | ||
if err != nil { | ||
return errutil.Wrapf(err, "getting executable path") | ||
} | ||
|
||
// Parse the plist template | ||
tmpl, err := template.New("plist").Parse(plistTemplate) | ||
if err != nil { | ||
return errutil.Wrapf(err, "parsing plist template") | ||
} | ||
|
||
// The path to the plist file | ||
plistPath, err := a.getPlistFilePath() | ||
if err != nil { | ||
return errutil.Wrap(err) | ||
} | ||
|
||
// Make the directory if it doesn't exist | ||
plistDir := filepath.Dir(plistPath) | ||
if _, err := os.Stat(plistDir); os.IsNotExist(err) { | ||
if err := os.MkdirAll(plistDir, 0777); err != nil { | ||
return errutil.Wrapf(err, "creating LaunchAgents directory") | ||
} | ||
} | ||
|
||
// Open the output file | ||
file, err := os.Create(plistPath) | ||
if err != nil { | ||
return errutil.Wrapf(err, "creating plist file") | ||
} | ||
defer file.Close() | ||
|
||
// Render into the file | ||
templateData := &plistTemplateData{ | ||
Options: a.options, | ||
BinaryPath: binaryPath, | ||
EscapedArguments: escapeArgs(a.options.Arguments), | ||
} | ||
return errutil.Wrapf(tmpl.Execute(file, templateData), "rendering plist template") | ||
} | ||
|
||
func (a *autostart) Disable() error { | ||
// Get the path to the plist file | ||
plistPath, err := a.getPlistFilePath() | ||
if err != nil { | ||
return errutil.Wrap(err) | ||
} | ||
|
||
// If the plist file exists, delete it | ||
if _, err := os.Stat(plistPath); err == nil { | ||
return errutil.Wrapf(os.Remove(plistPath), "deleting plist file") | ||
} | ||
return nil | ||
} | ||
|
||
func (a *autostart) getPlistDir() (string, error) { | ||
switch a.options.Mode { | ||
case ModeUser: | ||
return filepath.Join(os.Getenv("HOME"), "Library", "LaunchAgents"), nil | ||
case ModeSystem: | ||
return filepath.Join("/", "Library", "LaunchAgents"), nil | ||
default: | ||
return "", ErrInvalidMode | ||
} | ||
} | ||
|
||
func (a *autostart) getPlistFilePath() (string, error) { | ||
launchAgentsDir, err := a.getPlistDir() | ||
if err != nil { | ||
return "", errutil.Wrap(err) | ||
} | ||
return filepath.Join(launchAgentsDir, fmt.Sprintf("%s.plist", a.options.Label)), nil | ||
} | ||
|
||
func escapeArgs(arguments []string) []string { | ||
output := make([]string, len(arguments)) | ||
for i, arg := range arguments { | ||
output[i] = escapeXML(arg) | ||
} | ||
return output | ||
} | ||
|
||
func escapeXML(s string) string { | ||
s = strings.ReplaceAll(s, "&", "&") | ||
s = strings.ReplaceAll(s, "<", "<") | ||
s = strings.ReplaceAll(s, ">", ">") | ||
s = strings.ReplaceAll(s, "\"", """) | ||
s = strings.ReplaceAll(s, "'", "'") | ||
return s | ||
} |
Oops, something went wrong.