Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ A simple and lightweight process manager for Go applications. Meeseeks can be us
- **Dual Usage**: CLI tool and Go package
- **Process Management**: Start, stop, and monitor multiple processes
- **Daemon Mode**: Run processes in the background with Docker Compose-like commands
- **Auto-Start at Login**: Cross-platform service management for macOS, Linux (TODO), and Windows (TODO)
- **Configuration Files**: YAML and JSON support
- **Scheduled Execution**: Run processes at intervals
- **Real-time Monitoring**: Process status, logs, and statistics
- **Output Redirection**: Capture or redirect stdout/stderr
- **Graceful Shutdown**: Context-based cancellation and signal handling
- **Security-First**: Input validation, privilege minimization, and secure defaults

## Installation

Expand Down Expand Up @@ -75,6 +77,14 @@ meeseeks stop web-server # Stop specific program
meeseeks stop # Stop all programs
```

6. **Configure auto-start at login**:

```bash
meeseeks run-at-login enable
meeseeks run-at-login status
meeseeks run-at-login disable
```

### Go Package Usage

```go
Expand Down Expand Up @@ -263,6 +273,31 @@ Stop and programs and meeseks process. Useful to stop meeseeks when running in d
meeseeks exit
```

### `meeseeks run-at-login`

Manage automatic startup of meeseeks at user login across platforms.

```bash
# Enable auto-start
meeseeks run-at-login enable

# Check current status
meeseeks run-at-login status

# Disable auto-start
meeseeks run-at-login disable
```

**Platform Support:**
- **macOS**: Creates LaunchAgent plist files
- **Linux**: TODO
- **Windows**: TODO

**Security Features:**
- Runs only in user context (no admin privileges required)
- Input validation prevents injection attacks
- Secure file permissions and directory restrictions

### `meeseeks version`

Show version information.
Expand Down Expand Up @@ -310,7 +345,7 @@ programs:

### Production Monitoring

Simple process supervision and monitoring:
Simple process supervision with auto-start capability:

```yaml
programs:
Expand All @@ -329,6 +364,15 @@ programs:
interval: "5m"
```

**Enable auto-start at login:**
```bash
# Configure meeseeks to start automatically
meeseeks run-at-login enable

# Verify it's working
meeseeks run-at-login status
```

### Scheduled Tasks

Cron-like functionality with better process management:
Expand Down
2 changes: 1 addition & 1 deletion cmd/meeseeks/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestLogsCommand(t *testing.T) {
}

func TestLogsCommand_Help(t *testing.T) {
testCommandHelp(t, "logs", []string{
testCommandHelp(t, []string{"logs"}, []string{
"Usage: meeseeks logs <program_name>",
"Show logs for a specific program",
})
Expand Down
47 changes: 22 additions & 25 deletions cmd/meeseeks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,48 @@ func main() {

command := os.Args[1]
args := os.Args[2:]
var cmdErr error

logger := logger.New()

switch command {
case "run":
if err := runCommand(args, logger); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
cmdErr = runCommand(args, logger)
case "status":
if err := statusCommand(args, logger); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
cmdErr = statusCommand(args, logger)
case "logs":
if err := logsCommand(args, logger); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
cmdErr = logsCommand(args, logger)
case "stop":
if err := stopCommand(args, logger); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
cmdErr = stopCommand(args, logger)
case "exit":
if err := exitCommand(args, logger); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
cmdErr = exitCommand(args, logger)
case "run-at-login":
cmdErr = runAtLoginCommand(args, logger)
case "version":
fmt.Fprintln(os.Stdout, "meeseeks version 1.0.0")
case "-h", "--help":
printUsage()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
printUsage()
os.Exit(1)
}

if cmdErr != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", cmdErr)
os.Exit(1)
}
}

func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: meeseeks <command> [options]\n\n")
fmt.Fprintf(os.Stderr, "Commands:\n")
fmt.Fprintf(os.Stderr, " run Start programs from config file\n")
fmt.Fprintf(os.Stderr, " status Show status of running programs\n")
fmt.Fprintf(os.Stderr, " logs Show logs for a specific program\n")
fmt.Fprintf(os.Stderr, " stop Stop running programs\n")
fmt.Fprintf(os.Stderr, " version Show version information\n")
fmt.Fprintf(os.Stderr, " run Start programs from config file\n")
fmt.Fprintf(os.Stderr, " status Show status of running programs\n")
fmt.Fprintf(os.Stderr, " logs Show logs for a specific program\n")
fmt.Fprintf(os.Stderr, " stop Stop running programs\n")
fmt.Fprintf(os.Stderr, " run-at-login Manage automatic startup at user login\n")
fmt.Fprintf(os.Stderr, " exit Stop meeseeks program\n")
fmt.Fprintf(os.Stderr, " version Show version information\n")
fmt.Fprintf(os.Stderr, "\nUse 'meeseeks <command> -h' for more information about a command.\n")
}
11 changes: 6 additions & 5 deletions cmd/meeseeks/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ func TestMainCommands(t *testing.T) {

expectedMessages := []string{
"Usage: meeseeks <command>",
"run Start programs from config file",
"status Show status of running programs",
"logs Show logs for a specific program",
"stop Stop running programs",
"version Show version information",
"run Start programs from config file",
"status Show status of running programs",
"logs Show logs for a specific program",
"stop Stop running programs",
"run-at-login Manage automatic startup at user login",
"version Show version information",
}

for _, msg := range expectedMessages {
Expand Down
2 changes: 1 addition & 1 deletion cmd/meeseeks/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func startServer(
}

if err := s.Start(ctx); err != nil {
return nil, fmt.Errorf("failed to start daemon: %w", err)
return nil, fmt.Errorf("failed to start server: %w", err)
}

s.StartPrograms(ctx)
Expand Down
170 changes: 170 additions & 0 deletions cmd/meeseeks/run_at_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package main

import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"

"github.com/GustavoCaso/meeseeks/internal/logger"
"github.com/GustavoCaso/meeseeks/internal/login"
)

func runAtLoginCommand(args []string, logger *logger.Logger) error {
if len(args) < 1 {
printRunAtLoginUsage()
return errors.New("subcommand required")
}

// Handle help flag at the top level
if args[0] == "-h" || args[0] == "--help" {
printRunAtLoginUsage()
return nil
}

subcommand := args[0]
subcommandArgs := args[1:]

// Get platform-specific login service
service := login.GetService(logger)

switch subcommand {
case "enable":
return runAtLoginEnableCommand(service, subcommandArgs)
case "disable":
return runAtLoginDisableCommand(service, subcommandArgs)
case "status":
return runAtLoginStatusCommand(service, subcommandArgs)
default:
printRunAtLoginUsage()
return fmt.Errorf("unknown subcommand: %s", subcommand)
}
}

func runAtLoginEnableCommand(service login.Service, args []string) error {
fs := flag.NewFlagSet("run-at-login enable", flag.ExitOnError)

fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: meeseeks run-at-login enable\n\n")
fmt.Fprintf(os.Stderr, "Configure meeseeks to start automatically at login\n\n")
fs.PrintDefaults()
}

if err := fs.Parse(args); err != nil {
return err
}

configDir := getMeeseeksDir()

// Convert to absolute path if not already
absConfigDir, err := filepath.Abs(configDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for config dir: %w", err)
}

// Convert to absolute path if not already
absConfigPath, err := filepath.Abs(getDefaultConfigPath())
if err != nil {
return fmt.Errorf("failed to get absolute path for config file: %w", err)
}

// Get current executable path
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
absExecPath, absErr := filepath.Abs(execPath)
if absErr != nil {
return fmt.Errorf("failed to get absolute path for executable: %w", absErr)
}

// Create login service configuration
loginConfig := login.ServiceConfig{
ConfigPath: absConfigPath,
ExecutablePath: absExecPath,
ConfigDir: absConfigDir,
}

serviceDefnition, createErr := service.Create(loginConfig)

if createErr != nil {
return fmt.Errorf("failed to create login service: %w", createErr)
}

// Enable the service
if enableErr := service.Enable(serviceDefnition); enableErr != nil {
return fmt.Errorf("failed to enable login service: %w", enableErr)
}

fmt.Fprintf(os.Stdout, "Successfully enabled meeseeks to start at login\n")
fmt.Fprintf(os.Stdout, "Config file: %s\n", absConfigPath)

return nil
}

func runAtLoginDisableCommand(service login.Service, args []string) error {
fs := flag.NewFlagSet("run-at-login disable", flag.ExitOnError)

fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: meeseeks run-at-login disable\n\n")
fmt.Fprintf(os.Stderr, "Remove automatic startup configuration\n\n")
fs.PrintDefaults()
}

if err := fs.Parse(args); err != nil {
return err
}

// Disable the service
if err := service.Disable(); err != nil {
return fmt.Errorf("failed to disable login service: %w", err)
}

fmt.Fprintf(os.Stdout, "Successfully disabled meeseeks login service\n")

return nil
}

func runAtLoginStatusCommand(service login.Service, args []string) error {
fs := flag.NewFlagSet("run-at-login status", flag.ExitOnError)

fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: meeseeks run-at-login status\n\n")
fmt.Fprintf(os.Stderr, "Show current login service status\n\n")
fs.PrintDefaults()
}

if err := fs.Parse(args); err != nil {
return err
}

// Get service status
status, err := service.Status()
if err != nil {
return fmt.Errorf("failed to get login service status: %w", err)
}

fmt.Fprintf(os.Stdout, "Enabled: %t\n", status.Enabled)
fmt.Fprintf(os.Stdout, "Running: %t\n", status.Running)

if !status.LastRun.IsZero() {
fmt.Fprintf(os.Stdout, "Last Run: %s\n", status.LastRun.Format("2006-01-02 15:04:05"))
}

if status.Error != "" {
fmt.Fprintf(os.Stdout, "Error: %s\n", status.Error)
}

return nil
}

func printRunAtLoginUsage() {
fmt.Fprintf(os.Stderr, "Usage: meeseeks run-at-login <subcommand>\n\n")
fmt.Fprintf(os.Stderr, "Manage automatic startup of meeseeks at user login\n\n")
fmt.Fprintf(os.Stderr, "Subcommands:\n")
fmt.Fprintf(os.Stderr, " enable Configure meeseeks to start automatically at login\n")
fmt.Fprintf(os.Stderr, " disable Remove automatic startup configuration\n")
fmt.Fprintf(os.Stderr, " status Show current login service status\n\n")
fmt.Fprintf(os.Stderr, "Use 'meeseeks run-at-login <subcommand> -h' for more information about a subcommand.\n")
}
Loading