userservice

package
v0.8.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 24, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package userservice manages OS-level user services (daemons that run as the current user, independent of the calling process lifecycle).

Architecture

The userservice package provides a unified API across all platforms:

Create(name, desc, cmd) error
Start(name) error
Stop(name) error
Status(name) (string, error)
List() ([]string, error)
Delete(name) error
Scan() ([]string, error)
ScanStatus(name) (string, error)

Platform backends:

Linux      systemd --user   → pidfile fallback if systemd unavailable
macOS      launchctl        → no fallback needed
Other      none             → pidfile + direct process daemon

Registry: ~/.dscli/services/<name>.json Marker: "# Managed by dscli" / "<!-- Managed by dscli -->"

Backends

Platform   Primary Backend    Fallback              Config Directory
Linux      systemd --user     pidfile (if no        ~/.config/systemd/user/
                              systemd available)    ~/.dscli/services/
macOS      launchctl          n/a                   ~/Library/LaunchAgents/
                                                   ~/.dscli/services/
Other      n/a                pidfile + direct      ~/.dscli/services/
                              process daemon

Fallback

When systemd is unavailable on Linux, or on any non-Linux, non-macOS platform (Windows, FreeBSD, etc.), userservice falls back to direct process management:

  • Start: daemonizes the command (Setsid / CREATE_NEW_PROCESS_GROUP), redirects stdio to /dev/null, records PID in ~/.dscli/services/<name>.pid.
  • Stop: reads PID from pidfile, sends SIGTERM (taskkill on Windows), removes pidfile.
  • Status: checks process liveness via signal(0) (tasklist on Windows). Stale pidfiles are auto-cleaned.

All fallback operations are idempotent: calling start on a running service is a no-op, calling stop on a stopped service succeeds.

Registry

Every service created through userservice is recorded in a JSON registry at ~/.dscli/services/<name>.json:

{
  "name": "my-service",
  "desc": "My Service",
  "exec_start": "/usr/bin/my-service --flag",
  "args": ["/usr/bin/my-service", "--flag"]
}

The registry is the source of truth for List, Status, and Delete. The Args field (stored verbatim from *exec.Cmd.Args) enables type-safe command reconstruction without fragile whitespace splitting.

DScli Marker

Service unit files include a dscli marker for identification:

systemd:  # Managed by dscli     (first line of unit file)
launchd:  <!-- Managed by dscli --> (first line of plist)

The Scan function uses this marker to discover orphaned services (OS-level units without a corresponding JSON registry entry). Services created before the marker was introduced can be re-registered by running Create again (it is idempotent).

API

Create writes the platform-specific service configuration (systemd unit file, LaunchAgent plist, or registry-only for fallback platforms) and records it in the JSON registry. It resolves the binary path via LookPath so the config always carries an absolute path.

Create is idempotent: if the service file already exists with identical content, no changes are made. The JSON registry is always refreshed.

Start activates the service. On systemd: runs "systemctl --user start <name>". On macOS: runs "launchctl load <plist>" (RunAtLoad ensures the job starts). On fallback: daemonizes the command and records the PID.

Stop deactivates the service. On systemd: runs "systemctl --user stop <name>". On macOS: runs "launchctl unload <plist>". On fallback: sends SIGTERM/taskkill and removes the pidfile.

Delete removes all traces of the service: stops it if running, removes the platform-specific config files, and deletes the JSON registry entry (best-effort).

List returns the names of all dscli-managed services by scanning ~/.dscli/services/*.json. Returns an empty slice (not nil) when no services exist.

Status reports one of:

"running"   — service is active and config is fresh
"stale"     — config is out of date (dscli binary or config newer)
"stopped"   — config exists and is fresh, but service is not running
"not_found" — no registry entry for this name

"stale" indicates the service was created by an older dscli version or before a config change; it should be re-created to pick up updates.

Scan returns the names of services that exist at the OS level (systemd/ launchd units with the dscli marker) but lack a JSON registry entry. These "orphaned" services were likely created before the JSON registry was introduced, or their registry files were deleted.

ScanStatus is like Status but works even when the JSON registry entry is missing. It checks the OS-level service manager directly. Unlike Status, ScanStatus never returns "stale" (stale detection requires a registry entry).

Design Decisions

  1. Why not use github.com/kardianos/service? That library focuses on system services (root-level daemons) and carries significant complexity. userservice focuses exclusively on user-scoped services with a minimal API surface.

  2. Create takes *exec.Cmd, not a string. This avoids fragile string parsing of command lines (no shell-quoting or whitespace-splitting ambiguity). The public Create resolves cmd.Path via LookPath and persists cmd.Args verbatim so every backend reconstructs the command correctly.

  3. Create does NOT start the service. Create and Start are separate calls so callers can decide whether to start immediately or just ensure the config is deployed.

  4. fallback is not a global singleton. Each call constructs a new fallback{} instance, which carries no mutable state (all state lives in the pidfile and JSON registry on disk).

Usage

import "gitcode.com/dscli/dscli/internal/userservice"

cmd := exec.Command("/usr/bin/lightpanda", "serve", "--host", "127.2.2.9", "--port", "9227")
if err := userservice.Create("dscli-lightpanda", "Lightpanda Browser", cmd); err != nil {
    // handle
}
if err := userservice.Start("dscli-lightpanda"); err != nil {
    // handle
}

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = errors.New("userservice: platform not supported")

ErrUnsupported is returned when the platform has no service manager backend.

Functions

func Create

func Create(name, desc string, cmd *exec.Cmd) error

Create creates or updates a user service configuration.

On Linux: writes a systemd user unit file at ~/.config/systemd/user/<name>.service, runs daemon-reload + enable, then records the config at ~/.dscli/services/<name>.json.

On macOS: writes a LaunchAgent plist at ~/Library/LaunchAgents/<name>.plist, then records the config at ~/.dscli/services/<name>.json.

Create is idempotent: if the service file already exists with identical content, the file is not rewritten and no reload commands are run. The JSON registry is always refreshed.

Create resolves cmd.Path via LookPath so the service file always contains the absolute binary path. cmd.Args[0] is rewritten to the resolved path; on Linux the command line uses cmd.String(), while on macOS cmd.Args is used as ProgramArguments directly (no fragile whitespace splitting).

Parameters:

  • name: service name, used as filename stem and service identifier
  • desc: human-readable description (systemd Description / Launchd Label)
  • cmd: command to execute; Path must be non-empty and resolvable

func Delete

func Delete(name string) error

Delete removes the user service configuration and stops the service if it is running.

On Linux: runs "systemctl --user disable --now <name>" and removes the unit file, then daemon-reload. On macOS: runs "launchctl unload" and removes the plist file.

The JSON registry entry at ~/.dscli/services/<name>.json is also removed (best-effort).

func List

func List() ([]string, error)

List returns the names of all services managed by userservice.

It reads the dscli registry at ~/.dscli/services/ — only services created through userservice.Create are listed. Returns an empty slice (not nil) when the directory does not exist.

func Scan

func Scan() ([]string, error)

Scan returns the names of dscli-managed services that exist at the OS level (systemd/launchd) but have no corresponding JSON registry entry. These "orphaned" services were likely created before the JSON registry was introduced, or their registry files were deleted.

Use the --scan flag on "dscli service list" or "dscli service status" to include orphaned services. Orphaned services can be re-registered by running "dscli service create" again (it is idempotent).

func ScanStatus

func ScanStatus(name string) (string, error)

ScanStatus is like Status but works even when the JSON registry entry is missing. It checks the OS-level service manager directly.

Returns:

  • "running" — service is active at the OS level
  • "stopped" — service unit exists but is not running
  • "not_found" — no service found at the OS level either
  • "stale" — (never returned by ScanStatus — stale requires a registry entry)

func Start

func Start(name string) error

Start starts the user service.

On Linux: runs "systemctl --user start <name>". On macOS: runs "launchctl load <plist-path>" (loads and starts the job).

func Status

func Status(name string) (string, error)

Status returns a summary of the service's state:

  • "running" — service is active and config is fresh
  • "stale" — config is out of date (service may or may not be running)
  • "stopped" — config exists and is fresh, but service is not running
  • "not_found" — no service config found for this name

"not_found" is returned when the registry entry (~/.dscli/services/<name>.json) is missing. "stale" means the registry entry is older than the dscli binary or the dscli config file.

Status returns an error only when it cannot determine the state (e.g. home directory unavailable).

func Stop

func Stop(name string) error

Stop stops the user service.

On Linux: runs "systemctl --user stop <name>". On macOS: runs "launchctl unload <plist-path>" (stops and unloads the job).

Types

This section is empty.

Source Files

  • doc.go
  • fallback.go
  • fallback_unix.go
  • userservice.go
  • userservice_linux.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL