Skip to content

Commit

Permalink
Merge pull request #1 from spiretechnology/cdd/version-2
Browse files Browse the repository at this point in the history
- 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
connerdouglass authored Aug 9, 2023
2 parents c47ebb0 + 9cf38fc commit fd3bdcf
Show file tree
Hide file tree
Showing 20 changed files with 965 additions and 202 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/pull_request.yml
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 ./...
88 changes: 88 additions & 0 deletions README.md
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()
```
6 changes: 0 additions & 6 deletions app.go

This file was deleted.

128 changes: 128 additions & 0 deletions app_darwin.go
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, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
s = strings.ReplaceAll(s, "'", "&apos;")
return s
}
Loading

0 comments on commit fd3bdcf

Please sign in to comment.