Files
unitdore/systemd/manager.go

157 lines
4.2 KiB
Go

package systemd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/warkanum/unitdore/config"
)
const systemUnitDir = "/etc/systemd/system"
// UnitPath returns the filesystem path where the .service file should be written.
func UnitPath(u config.Unit, prefix, suffix string) (string, error) {
svcName := ServiceName(u, prefix, suffix)
if u.User == "" {
return filepath.Join(systemUnitDir, svcName), nil
}
// Rootless: write to the user's systemd config dir
home, err := userHomeDir(u.User)
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "systemd", "user", svcName), nil
}
// Install writes the .service file for a unit and reloads systemd.
func Install(u config.Unit, prefix, suffix, serviceUser string) error {
content, err := Generate(u, prefix, suffix, serviceUser)
if err != nil {
return err
}
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("creating unit dir: %w", err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("writing unit file: %w", err)
}
return daemonReload(u.User)
}
// Uninstall removes the .service file for a unit and reloads systemd.
func Uninstall(u config.Unit, prefix, suffix string) error {
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing unit file: %w", err)
}
return daemonReload(u.User)
}
// Enable enables and starts a unit.
func Enable(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "enable", "--now", ServiceName(u, prefix, suffix))
}
// Disable disables and stops a unit.
func Disable(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "disable", "--now", ServiceName(u, prefix, suffix))
}
// Start starts a unit without enabling it.
func Start(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "start", ServiceName(u, prefix, suffix))
}
// Stop stops a unit without disabling it.
func Stop(u config.Unit, prefix, suffix string) error {
return systemctl(u.User, "stop", ServiceName(u, prefix, suffix))
}
// Status returns the ActiveState of a unit ("active", "inactive", "failed", "unknown").
func Status(u config.Unit, prefix, suffix string) string {
args := []string{"show", "-p", "ActiveState", "--value", ServiceName(u, prefix, suffix)}
var out []byte
var err error
if u.User == "" {
out, err = exec.Command("systemctl", args...).Output()
} else {
out, err = runAsUser(u.User, "systemctl", append([]string{"--user"}, args...)...)
}
if err != nil {
return "unknown"
}
return strings.TrimSpace(string(out))
}
// IsInstalled returns true if the unit file exists on disk.
func IsInstalled(u config.Unit, prefix, suffix string) bool {
path, err := UnitPath(u, prefix, suffix)
if err != nil {
return false
}
_, err = os.Stat(path)
return err == nil
}
// --- helpers ---
func daemonReload(user string) error {
if user == "" {
return systemctl("", "daemon-reload")
}
return systemctl(user, "daemon-reload")
}
func systemctl(user string, args ...string) error {
if user == "" {
cmd := exec.Command("systemctl", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
return runAsUserCmd(user, "systemctl", append([]string{"--user"}, args...)...)
}
func runAsUser(user string, bin string, args ...string) ([]byte, error) {
full := append([]string{"-u", user, "--", bin}, args...)
return exec.Command("runuser", full...).Output()
}
func runAsUserCmd(user string, bin string, args ...string) error {
full := append([]string{"-u", user, "--", bin}, args...)
cmd := exec.Command("runuser", full...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func userHomeDir(user string) (string, error) {
out, err := exec.Command("getent", "passwd", user).Output()
if err != nil {
return "", fmt.Errorf("looking up user %s: %w", user, err)
}
// passwd format: name:pass:uid:gid:gecos:home:shell
parts := strings.Split(strings.TrimSpace(string(out)), ":")
if len(parts) < 6 {
return "", fmt.Errorf("unexpected passwd entry for %s", user)
}
return parts[5], nil
}