feat: order/delay in service files, enable cmd, --all on unit commands
- install/uninstall/start/stop/enable take <name> or --all; *all kept as shortcuts - add enable (enable at boot + start); startall no longer enables - order/delay resolved by config.ResolveOrder and written into systemd, OpenRC and runit service files (After=/sleep), so they apply at boot - install --all stops and disables units before removing their files - update regenerates all installed, enabled units and removes disabled ones - edit creates the config dir; docs and man page updated - tests: fake Init for cmd, ResolveOrder, backend ordering output
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
// The *all commands are kept as shortcuts for '<command> --all'.
|
||||
var (
|
||||
startallCmd = &cobra.Command{
|
||||
Use: "startall",
|
||||
Short: "Start all installed, enabled units (same as start --all)",
|
||||
RunE: bulkRun(func(cfg *config.Config, ini initsys.Init) {
|
||||
bulkStart(cfg, ini, os.Stdout, initsys.Init.Start, "started")
|
||||
}),
|
||||
}
|
||||
enableallCmd = &cobra.Command{
|
||||
Use: "enableall",
|
||||
Short: "Enable at boot and start all installed, enabled units (same as enable --all)",
|
||||
RunE: bulkRun(func(cfg *config.Config, ini initsys.Init) {
|
||||
bulkStart(cfg, ini, os.Stdout, initsys.Init.Enable, "enabled")
|
||||
}),
|
||||
}
|
||||
stopallCmd = &cobra.Command{
|
||||
Use: "stopall",
|
||||
Short: "Stop all installed units (same as stop --all)",
|
||||
RunE: bulkRun(func(cfg *config.Config, ini initsys.Init) { stopAll(cfg, ini, os.Stdout) }),
|
||||
}
|
||||
uninstallallCmd = &cobra.Command{
|
||||
Use: "uninstallall",
|
||||
Short: "Stop, disable and remove all installed units (same as uninstall --all)",
|
||||
RunE: bulkRun(func(cfg *config.Config, ini initsys.Init) { uninstallAll(cfg, ini, os.Stdout) }),
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(startallCmd, enableallCmd, stopallCmd, uninstallallCmd)
|
||||
}
|
||||
|
||||
func bulkRun(fn func(*config.Config, initsys.Init)) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
cfg, ini, err := loadInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn(cfg, ini)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type unitAction func(ini initsys.Init, u config.Unit, prefix, suffix string) error
|
||||
|
||||
// bulkStart applies act to all enabled, installed units in startup order.
|
||||
func bulkStart(cfg *config.Config, ini initsys.Init, w io.Writer, act unitAction, past string) {
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
var todo []config.Unit
|
||||
for _, u := range sortUnits(cfg.Units, false) {
|
||||
if !u.Enabled {
|
||||
continue
|
||||
}
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
fmt.Fprintf(w, " ! %s: not installed — run 'unitdore install %s' first\n", u.Name, u.Name)
|
||||
continue
|
||||
}
|
||||
todo = append(todo, u)
|
||||
}
|
||||
|
||||
ok, failed := runOrdered(todo, w, func(u config.Unit) (string, error) {
|
||||
if err := act(ini, u, prefix, suffix); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return past + ": " + u.Name, nil
|
||||
})
|
||||
fmt.Fprintf(w, "\nDone. %s: %d Failed: %d\n", strings.ToUpper(past[:1])+past[1:], ok, failed)
|
||||
}
|
||||
|
||||
// stopAll stops every installed unit in reverse startup order.
|
||||
func stopAll(cfg *config.Config, ini initsys.Init, w io.Writer) {
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
stopped, failed := 0, 0
|
||||
for _, u := range sortUnits(cfg.Units, true) {
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " ■ stopping: %s...\n", ini.ServiceName(u, prefix, suffix))
|
||||
if err := ini.Stop(u, prefix, suffix); err != nil {
|
||||
fmt.Fprintf(w, " ✗ failed: %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ stopped: %s\n", u.Name)
|
||||
stopped++
|
||||
}
|
||||
fmt.Fprintf(w, "\nDone. Stopped: %d Failed: %d\n", stopped, failed)
|
||||
}
|
||||
|
||||
// uninstallAll disables and removes every installed unit in reverse startup order.
|
||||
func uninstallAll(cfg *config.Config, ini initsys.Init, w io.Writer) {
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
removed, failed := 0, 0
|
||||
|
||||
for _, u := range sortUnits(cfg.Units, true) {
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " ■ disabling %s...\n", ini.ServiceName(u, prefix, suffix))
|
||||
if err := ini.Disable(u, prefix, suffix); err != nil {
|
||||
fmt.Fprintf(w, " ✗ failed to disable %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Fprintf(w, " ✗ removing %s...\n", path)
|
||||
if err := ini.Uninstall(u, prefix, suffix); err != nil {
|
||||
fmt.Fprintf(w, " ✗ failed to remove %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ uninstalled: %s\n", u.Name)
|
||||
removed++
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\nDone. Uninstalled: %d Failed: %d\n", removed, failed)
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
// fakeInit records calls and simulates installed files in memory.
|
||||
type fakeInit struct {
|
||||
mu sync.Mutex
|
||||
installed map[string]bool
|
||||
calls []string
|
||||
failOn map[string]bool // "op:name" -> error
|
||||
}
|
||||
|
||||
func newFake(installed ...string) *fakeInit {
|
||||
f := &fakeInit{installed: map[string]bool{}, failOn: map[string]bool{}}
|
||||
for _, n := range installed {
|
||||
f.installed[n] = true
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fakeInit) rec(op string, u config.Unit) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.calls = append(f.calls, op+":"+u.Name)
|
||||
if f.failOn[op+":"+u.Name] {
|
||||
return errors.New("boom")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInit) Name() string { return "fake" }
|
||||
func (f *fakeInit) ServiceName(u config.Unit, p, s string) string { return p + u.Name + s }
|
||||
func (f *fakeInit) UnitPath(u config.Unit, p, s string) (string, error) {
|
||||
return "/fake/" + f.ServiceName(u, p, s), nil
|
||||
}
|
||||
func (f *fakeInit) Generate(u config.Unit, p, s string) (string, error) {
|
||||
return "content-" + u.Name, nil
|
||||
}
|
||||
func (f *fakeInit) Install(u config.Unit, p, s string) error {
|
||||
if err := f.rec("install", u); err != nil {
|
||||
return err
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.installed[u.Name] = true
|
||||
f.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (f *fakeInit) Uninstall(u config.Unit, p, s string) error {
|
||||
if err := f.rec("uninstall", u); err != nil {
|
||||
return err
|
||||
}
|
||||
f.mu.Lock()
|
||||
delete(f.installed, u.Name)
|
||||
f.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (f *fakeInit) Enable(u config.Unit, p, s string) error { return f.rec("enable", u) }
|
||||
func (f *fakeInit) Disable(u config.Unit, p, s string) error { return f.rec("disable", u) }
|
||||
func (f *fakeInit) Start(u config.Unit, p, s string) error { return f.rec("start", u) }
|
||||
func (f *fakeInit) Stop(u config.Unit, p, s string) error { return f.rec("stop", u) }
|
||||
func (f *fakeInit) Status(u config.Unit, p, s string) string { return "active" }
|
||||
func (f *fakeInit) IsInstalled(u config.Unit, p, s string) bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.installed[u.Name]
|
||||
}
|
||||
func (f *fakeInit) SupportsLinger() bool { return false }
|
||||
|
||||
func unit(name string, order int, delay string, enabled bool) config.Unit {
|
||||
return config.Unit{Name: name, Runtime: "podman", Order: order, Delay: delay, Enabled: enabled}
|
||||
}
|
||||
|
||||
func TestSortUnits(t *testing.T) {
|
||||
in := []config.Unit{unit("b", 2, "", true), unit("a", 2, "", true), unit("c", 1, "", true)}
|
||||
names := func(us []config.Unit) (n []string) {
|
||||
for _, u := range us {
|
||||
n = append(n, u.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got := names(sortUnits(in, false)); !reflect.DeepEqual(got, []string{"c", "a", "b"}) {
|
||||
t.Errorf("forward = %v", got)
|
||||
}
|
||||
if got := names(sortUnits(in, true)); !reflect.DeepEqual(got, []string{"b", "a", "c"}) {
|
||||
t.Errorf("reverse = %v", got)
|
||||
}
|
||||
if in[0].Name != "b" {
|
||||
t.Error("input was mutated")
|
||||
}
|
||||
}
|
||||
|
||||
// grouped reports calls with each parallel group sorted, so order within a group is irrelevant.
|
||||
func grouped(calls []string, sizes ...int) [][]string {
|
||||
var out [][]string
|
||||
i := 0
|
||||
for _, n := range sizes {
|
||||
g := append([]string(nil), calls[i:i+n]...)
|
||||
sort.Strings(g)
|
||||
out = append(out, g)
|
||||
i += n
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestStartAllOrder(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{
|
||||
unit("db", 1, "", true),
|
||||
unit("web", 2, "3s", true),
|
||||
unit("web2", 2, "5s", true),
|
||||
unit("off", 2, "", false),
|
||||
unit("app", 3, "1s", true),
|
||||
unit("missing", 3, "", true),
|
||||
}}
|
||||
f := newFake("db", "web", "web2", "off", "app")
|
||||
|
||||
var out bytes.Buffer
|
||||
bulkStart(cfg, f, &out, initsys.Init.Start, "started")
|
||||
|
||||
want := [][]string{{"start:db"}, {"start:web", "start:web2"}, {"start:app"}}
|
||||
if got := grouped(f.calls, 1, 2, 1); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
o := out.String()
|
||||
for _, s := range []string{"! missing: not installed", "Done. Started: 4 Failed: 0"} {
|
||||
if !strings.Contains(o, s) {
|
||||
t.Errorf("output missing %q:\n%s", s, o)
|
||||
}
|
||||
}
|
||||
if strings.Contains(o, "off") {
|
||||
t.Errorf("disabled unit should be skipped:\n%s", o)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartAllParallelWithinGroup(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 1, "", true)}}
|
||||
f := newFake("a", "b")
|
||||
started := make(chan string, 2)
|
||||
release := make(chan struct{})
|
||||
act := func(_ initsys.Init, u config.Unit, _, _ string) error {
|
||||
started <- u.Name
|
||||
<-release // blocks until both have started: fails (deadlocks) if sequential
|
||||
return nil
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() { bulkStart(cfg, f, &bytes.Buffer{}, act, "started"); close(done) }()
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("units in the same order did not start in parallel")
|
||||
}
|
||||
}
|
||||
close(release)
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestEnableAllUsesEnable(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true)}}
|
||||
f := newFake("a")
|
||||
var out bytes.Buffer
|
||||
bulkStart(cfg, f, &out, initsys.Init.Enable, "enabled")
|
||||
if !reflect.DeepEqual(f.calls, []string{"enable:a"}) || !strings.Contains(out.String(), "Done. Enabled: 1") {
|
||||
t.Errorf("calls=%v out=%s", f.calls, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartAllFailureCounted(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", true)}}
|
||||
f := newFake("a", "b")
|
||||
f.failOn["start:a"] = true
|
||||
var out bytes.Buffer
|
||||
bulkStart(cfg, f, &out, initsys.Init.Start, "started")
|
||||
if !strings.Contains(out.String(), "Done. Started: 1 Failed: 1") {
|
||||
t.Errorf("out=%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopAllReverseOrder(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", false), unit("c", 3, "", true), unit("d", 3, "", true)}}
|
||||
f := newFake("a", "b", "c", "d")
|
||||
var out bytes.Buffer
|
||||
stopAll(cfg, f, &out)
|
||||
if want := []string{"stop:d", "stop:c", "stop:b", "stop:a"}; !reflect.DeepEqual(f.calls, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
if !strings.Contains(out.String(), "Stopped: 4") {
|
||||
t.Errorf("out=%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallAllReverseOrderAndFailure(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", true), unit("c", 3, "", true)}}
|
||||
f := newFake("a", "b") // c not installed
|
||||
f.failOn["disable:b"] = true
|
||||
var out bytes.Buffer
|
||||
uninstallAll(cfg, f, &out)
|
||||
if want := []string{"disable:b", "disable:a", "uninstall:a"}; !reflect.DeepEqual(f.calls, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
if !f.installed["b"] {
|
||||
t.Error("b must stay installed when disable fails")
|
||||
}
|
||||
if !strings.Contains(out.String(), "Uninstalled: 1 Failed: 1") {
|
||||
t.Errorf("out=%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAll(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{
|
||||
unit("on", 1, "", true),
|
||||
unit("off-installed", 2, "", false),
|
||||
unit("off-absent", 3, "", false),
|
||||
}}
|
||||
f := newFake("off-installed")
|
||||
var out bytes.Buffer
|
||||
installAll(cfg, f, false, &out)
|
||||
|
||||
// a disabled unit must be stopped before its file is removed
|
||||
if want := []string{"install:on", "disable:off-installed", "uninstall:off-installed"}; !reflect.DeepEqual(f.calls, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
if !strings.Contains(out.String(), "Installed: 1 Removed: 1 Skipped: 1") {
|
||||
t.Errorf("out=%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAllDisableFailureKeepsFile(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("x", 1, "", false)}}
|
||||
f := newFake("x")
|
||||
f.failOn["disable:x"] = true
|
||||
installAll(cfg, f, false, &bytes.Buffer{})
|
||||
if !f.installed["x"] || len(f.calls) != 1 {
|
||||
t.Errorf("file must be kept when disable fails; calls=%v", f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAllDryRunWritesNothing(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("on", 1, "", true), unit("off", 2, "", false)}}
|
||||
f := newFake("off")
|
||||
var out bytes.Buffer
|
||||
installAll(cfg, f, true, &out)
|
||||
if len(f.calls) != 0 {
|
||||
t.Errorf("dry run touched system: %v", f.calls)
|
||||
}
|
||||
for _, s := range []string{"--- /fake/on ---", "content-on", "would remove: off", "Would install: 1"} {
|
||||
if !strings.Contains(out.String(), s) {
|
||||
t.Errorf("output missing %q:\n%s", s, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateCfg(t *testing.T) (*config.Config, string) {
|
||||
t.Helper()
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true)}}
|
||||
return cfg, filepath.Join(t.TempDir(), "units.yaml")
|
||||
}
|
||||
|
||||
func TestUpdateOrderReinstallsInstalled(t *testing.T) {
|
||||
cfg, path := updateCfg(t)
|
||||
f := newFake("a")
|
||||
order := 7
|
||||
if err := applyUpdate(cfg, f, "a", &order, false, false, path, &bytes.Buffer{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(f.calls, []string{"install:a"}) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
saved, err := config.Load(path)
|
||||
if err != nil || saved.Units[0].Order != 7 {
|
||||
t.Errorf("saved = %+v, %v", saved, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDisableStopsAndRemoves(t *testing.T) {
|
||||
cfg, path := updateCfg(t)
|
||||
f := newFake("a")
|
||||
if err := applyUpdate(cfg, f, "a", nil, false, true, path, &bytes.Buffer{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"disable:a", "uninstall:a"}; !reflect.DeepEqual(f.calls, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
saved, _ := config.Load(path)
|
||||
if saved.Units[0].Enabled {
|
||||
t.Error("unit should be saved as disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNotInstalledOnlySaves(t *testing.T) {
|
||||
cfg, path := updateCfg(t)
|
||||
f := newFake()
|
||||
if err := applyUpdate(cfg, f, "a", nil, false, true, path, &bytes.Buffer{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.calls) != 0 {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateEnableClearsReason(t *testing.T) {
|
||||
cfg, path := updateCfg(t)
|
||||
cfg.Units[0].Enabled = false
|
||||
cfg.Units[0].DisabledReason = "container not found"
|
||||
if err := applyUpdate(cfg, newFake(), "a", nil, true, false, path, &bytes.Buffer{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved, _ := config.Load(path)
|
||||
if !saved.Units[0].Enabled || saved.Units[0].DisabledReason != "" {
|
||||
t.Errorf("saved = %+v", saved.Units[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUnknownUnit(t *testing.T) {
|
||||
cfg, path := updateCfg(t)
|
||||
if err := applyUpdate(cfg, newFake(), "nope", nil, false, false, path, &bytes.Buffer{}); err == nil {
|
||||
t.Error("expected not found error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOrderRefreshesNeighbours(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", true), unit("off", 3, "", false)}}
|
||||
path := filepath.Join(t.TempDir(), "units.yaml")
|
||||
f := newFake("a", "b", "off")
|
||||
order := 5
|
||||
if err := applyUpdate(cfg, f, "a", &order, false, false, path, &bytes.Buffer{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// b's After= changes when a moves behind it; disabled units are left alone
|
||||
if want := []string{"install:a", "install:b"}; !reflect.DeepEqual(f.calls, want) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
if got := cfg.Units[0].After; len(got) != 1 || got[0] != "b" {
|
||||
t.Errorf("a.After = %v, want [b]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallOneOnlyNamedUnit(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", true), unit("off", 3, "", false)}}
|
||||
f := newFake("off")
|
||||
var out bytes.Buffer
|
||||
if err := installOne(cfg, f, "b", false, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(f.calls, []string{"install:b"}) {
|
||||
t.Errorf("calls = %v", f.calls)
|
||||
}
|
||||
if !strings.Contains(out.String(), "installed: /fake/b") {
|
||||
t.Errorf("out=%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallOneDryRun(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true)}}
|
||||
f := newFake()
|
||||
var out bytes.Buffer
|
||||
if err := installOne(cfg, f, "a", true, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.calls) != 0 || !strings.Contains(out.String(), "content-a") {
|
||||
t.Errorf("calls=%v out=%s", f.calls, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallOneErrors(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("off", 1, "", false), unit("bad", 2, "", true)}}
|
||||
f := newFake("off")
|
||||
if err := installOne(cfg, f, "nope", false, &bytes.Buffer{}); err == nil {
|
||||
t.Error("unknown unit must error")
|
||||
}
|
||||
if err := installOne(cfg, f, "off", false, &bytes.Buffer{}); err == nil {
|
||||
t.Error("disabled unit must error")
|
||||
}
|
||||
f.failOn["install:bad"] = true
|
||||
if err := installOne(cfg, f, "bad", false, &bytes.Buffer{}); err == nil {
|
||||
t.Error("install failure must be returned")
|
||||
}
|
||||
if len(f.calls) != 1 || !f.installed["off"] {
|
||||
t.Errorf("disabled unit must be left untouched; calls=%v", f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameOrAll(t *testing.T) {
|
||||
cases := []struct {
|
||||
all bool
|
||||
args []string
|
||||
ok bool
|
||||
}{
|
||||
{false, nil, false},
|
||||
{false, []string{"a", "b"}, false},
|
||||
{false, []string{"a"}, true},
|
||||
{true, nil, true},
|
||||
{true, []string{"a"}, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
all := c.all
|
||||
if err := nameOrAll(&all)(nil, c.args); (err == nil) != c.ok {
|
||||
t.Errorf("all=%v args=%v: err=%v", c.all, c.args, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleUnitCommands(t *testing.T) {
|
||||
cfg := &config.Config{Units: []config.Unit{unit("a", 1, "", true), unit("b", 2, "", true), unit("gone", 3, "", true)}}
|
||||
f := newFake("a", "b")
|
||||
var out bytes.Buffer
|
||||
|
||||
for _, step := range []struct {
|
||||
op string
|
||||
run func(*config.Config, initsys.Init, string, io.Writer) error
|
||||
want string
|
||||
}{
|
||||
{"start", startOne, "start:a"},
|
||||
{"enable", enableOne, "enable:a"},
|
||||
{"stop", stopOne, "stop:a"},
|
||||
{"uninstall", uninstallOne, "uninstall:a"},
|
||||
} {
|
||||
f.calls = nil
|
||||
f.installed["a"] = true
|
||||
if err := step.run(cfg, f, "a", &out); err != nil {
|
||||
t.Fatalf("%s: %v", step.op, err)
|
||||
}
|
||||
// only the named unit is touched
|
||||
for _, c := range f.calls {
|
||||
if strings.HasSuffix(c, ":b") {
|
||||
t.Errorf("%s touched b: %v", step.op, f.calls)
|
||||
}
|
||||
}
|
||||
if got := f.calls[len(f.calls)-1]; got != step.want {
|
||||
t.Errorf("%s last call = %s, want %s", step.op, got, step.want)
|
||||
}
|
||||
if step.op == "uninstall" && !reflect.DeepEqual(f.calls, []string{"disable:a", "uninstall:a"}) {
|
||||
t.Errorf("uninstall calls = %v", f.calls)
|
||||
}
|
||||
|
||||
f.calls = nil
|
||||
if err := step.run(cfg, f, "gone", &out); err == nil {
|
||||
t.Errorf("%s: not-installed unit must error", step.op)
|
||||
}
|
||||
if err := step.run(cfg, f, "nope", &out); err == nil {
|
||||
t.Errorf("%s: unknown unit must error", step.op)
|
||||
}
|
||||
if len(f.calls) != 0 {
|
||||
t.Errorf("%s: errors must not act: %v", step.op, f.calls)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -31,6 +32,9 @@ func runEdit(cmd *cobra.Command, args []string) error {
|
||||
if editor == "" {
|
||||
return fmt.Errorf("no editor found; set $EDITOR")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("creating config dir: %w", err)
|
||||
}
|
||||
c := exec.Command(editor, configPath)
|
||||
c.Stdin = os.Stdin
|
||||
c.Stdout = os.Stdout
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var enableAll bool
|
||||
|
||||
var enableCmd = &cobra.Command{
|
||||
Use: "enable <name> | --all",
|
||||
Short: "Enable at boot and start a unit (or all with --all)",
|
||||
Long: `Enable enables the named installed unit at boot and starts it.
|
||||
|
||||
With --all (same as 'enableall') it does so for every enabled, installed unit
|
||||
in startup order, like 'start --all'.`,
|
||||
Args: nameOrAll(&enableAll),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, ini, err := loadInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enableAll {
|
||||
bulkStart(cfg, ini, os.Stdout, initsys.Init.Enable, "enabled")
|
||||
return nil
|
||||
}
|
||||
return enableOne(cfg, ini, args[0], os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
enableCmd.Flags().BoolVar(&enableAll, "all", false, "enable all enabled, installed units")
|
||||
rootCmd.AddCommand(enableCmd)
|
||||
}
|
||||
|
||||
func enableOne(cfg *config.Config, ini initsys.Init, name string, w io.Writer) error {
|
||||
u, err := installedUnit(cfg, ini, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, " ▶ enabling %s...\n", ini.ServiceName(*u, cfg.Prefix, cfg.Suffix))
|
||||
if err := ini.Enable(*u, cfg.Prefix, cfg.Suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ enabled: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
+46
-2
@@ -1,7 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
@@ -17,10 +20,51 @@ func runtimeLabel(u config.Unit) string {
|
||||
}
|
||||
|
||||
// lingerHint prints, once per user, how to make user units start at boot.
|
||||
func lingerHint(ini initsys.Init, seen map[string]bool, u config.Unit) {
|
||||
func lingerHint(w io.Writer, ini initsys.Init, seen map[string]bool, u config.Unit) {
|
||||
if !ini.SupportsLinger() || u.User == "" || seen[u.User] || sysuser.LingerEnabled(u.User) {
|
||||
return
|
||||
}
|
||||
seen[u.User] = true
|
||||
fmt.Printf(" ! linger not enabled for %s — user units won't start at boot: sudo loginctl enable-linger %s\n", u.User, u.User)
|
||||
fmt.Fprintf(w, " ! linger not enabled for %s — user units won't start at boot: sudo loginctl enable-linger %s\n", u.User, u.User)
|
||||
}
|
||||
|
||||
// nameOrAll requires exactly one unit name, or none when --all is set.
|
||||
func nameOrAll(all *bool) cobra.PositionalArgs {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if *all {
|
||||
if len(args) != 0 {
|
||||
return errors.New("--all takes no unit name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return errors.New("requires a unit name, or --all")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// installedUnit finds a unit by name and checks that its service file exists.
|
||||
func installedUnit(cfg *config.Config, ini initsys.Init, name string) (*config.Unit, error) {
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return nil, fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
if !ini.IsInstalled(*u, cfg.Prefix, cfg.Suffix) {
|
||||
return nil, fmt.Errorf("%s is not installed — run 'unitdore install %s' first", name, name)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// loadInit loads the config and selects the init backend.
|
||||
func loadInit() (*config.Config, initsys.Init, error) {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return cfg, ini, nil
|
||||
}
|
||||
|
||||
+121
-39
@@ -2,27 +2,48 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var dryRun bool
|
||||
var dryRun, installAllFlag bool
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Generate and install systemd unit files for all enabled units",
|
||||
Long: `Install generates .service files for all enabled units and writes them
|
||||
to the appropriate systemd directory. It does not enable or start them.
|
||||
Use: "install <name> | --all",
|
||||
Short: "Generate and install the service file for one unit (or all with --all)",
|
||||
Long: `Install generates the service file for the named unit using the configured
|
||||
init system. It does not enable or start it. The unit must be enabled in the
|
||||
config.
|
||||
|
||||
Use --dry-run to preview the generated unit files without writing anything.`,
|
||||
With --all it installs every enabled unit (same as 'installall'); disabled
|
||||
units that are installed are stopped, disabled and removed.
|
||||
|
||||
Use --dry-run to preview the generated files without writing anything.`,
|
||||
Args: nameOrAll(&installAllFlag),
|
||||
RunE: runInstall,
|
||||
}
|
||||
|
||||
var installallCmd = &cobra.Command{
|
||||
Use: "installall",
|
||||
Short: "Generate and install service files for all enabled units",
|
||||
Long: `Installall generates service files for all enabled units using the configured
|
||||
init system. It does not enable or start them. Units that are disabled in the
|
||||
config are stopped, disabled and their service files removed.
|
||||
|
||||
Use --dry-run to preview the generated files without writing anything.`,
|
||||
RunE: runInstallall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
installCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview unit files without writing")
|
||||
rootCmd.AddCommand(installCmd)
|
||||
installCmd.Flags().BoolVar(&installAllFlag, "all", false, "install all enabled units")
|
||||
for _, c := range []*cobra.Command{installCmd, installallCmd} {
|
||||
c.Flags().BoolVar(&dryRun, "dry-run", false, "preview service files without writing")
|
||||
rootCmd.AddCommand(c)
|
||||
}
|
||||
}
|
||||
|
||||
func runInstall(cmd *cobra.Command, args []string) error {
|
||||
@@ -30,61 +51,122 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := 0
|
||||
skipped := 0
|
||||
removed := 0
|
||||
if installAllFlag {
|
||||
installAll(cfg, ini, dryRun, os.Stdout)
|
||||
return nil
|
||||
}
|
||||
return installOne(cfg, ini, args[0], dryRun, os.Stdout)
|
||||
}
|
||||
|
||||
func runInstallall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installAll(cfg, ini, dryRun, os.Stdout)
|
||||
return nil
|
||||
}
|
||||
|
||||
// installOne installs a single enabled unit. Disabled units are an error:
|
||||
// removing files is installall's and update's job.
|
||||
func installOne(cfg *config.Config, ini initsys.Init, name string, dry bool, w io.Writer) error {
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
if !u.Enabled {
|
||||
return fmt.Errorf("unit %q is disabled — run 'unitdore update %s --enable' first", name, name)
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
path, err := ini.UnitPath(*u, prefix, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dry {
|
||||
content, err := ini.Generate(*u, prefix, suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, "\n--- %s ---\n%s\n", path, content)
|
||||
return nil
|
||||
}
|
||||
if err := ini.Install(*u, prefix, suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ installed: %s\n", path)
|
||||
lingerHint(w, ini, map[string]bool{}, *u)
|
||||
return nil
|
||||
}
|
||||
|
||||
func installAll(cfg *config.Config, ini initsys.Init, dry bool, w io.Writer) {
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
installed, skipped, removed := 0, 0, 0
|
||||
lingerSeen := map[string]bool{}
|
||||
|
||||
for _, u := range cfg.Units {
|
||||
name := ini.ServiceName(u, prefix, suffix)
|
||||
|
||||
if !u.Enabled {
|
||||
// Remove unit file if it exists for disabled units
|
||||
if ini.IsInstalled(u, prefix, suffix) {
|
||||
if dryRun {
|
||||
fmt.Printf(" ~ would remove: %s\n", ini.ServiceName(u, prefix, suffix))
|
||||
} else {
|
||||
if err := ini.Uninstall(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
|
||||
} else {
|
||||
fmt.Printf(" - removed: %s (disabled)\n", ini.ServiceName(u, prefix, suffix))
|
||||
removed++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if dry {
|
||||
fmt.Fprintf(w, " ~ would remove: %s\n", name)
|
||||
continue
|
||||
}
|
||||
// Stop and disable first so the service doesn't outlive its file
|
||||
if err := ini.Disable(u, prefix, suffix); err != nil {
|
||||
fmt.Fprintf(w, " ✗ failed to disable %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
if err := ini.Uninstall(u, prefix, suffix); err != nil {
|
||||
fmt.Fprintf(w, " ✗ failed to remove %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " - removed: %s (disabled)\n", name)
|
||||
removed++
|
||||
continue
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
if dry {
|
||||
content, err := ini.Generate(u, prefix, suffix)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ %s: %v\n", u.Name, err)
|
||||
fmt.Fprintf(w, " ✗ %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Printf("\n--- %s ---\n%s\n", path, content)
|
||||
path, err := ini.UnitPath(u, prefix, suffix)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, " ✗ %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "\n--- %s ---\n%s\n", path, content)
|
||||
installed++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ini.Install(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
|
||||
} else {
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Printf(" ✓ installed: %s\n", path)
|
||||
lingerHint(ini, lingerSeen, u)
|
||||
installed++
|
||||
fmt.Fprintf(w, " ✗ failed: %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Fprintf(w, " ✓ installed: %s\n", path)
|
||||
lingerHint(w, ini, lingerSeen, u)
|
||||
installed++
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
fmt.Printf("\nDone. Installed: %d Removed: %d Skipped: %d\n", installed, removed, skipped)
|
||||
verb := "Installed"
|
||||
if dry {
|
||||
verb = "Would install"
|
||||
}
|
||||
return nil
|
||||
fmt.Fprintf(w, "\nDone. %s: %d Removed: %d Skipped: %d\n", verb, installed, removed, skipped)
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var installallDryRun bool
|
||||
|
||||
var installallCmd = &cobra.Command{
|
||||
Use: "installall",
|
||||
Short: "Generate and install systemd unit files for all enabled units",
|
||||
Long: `Installall generates .service files for all enabled units and writes them
|
||||
to the appropriate systemd directory. It does not enable or start them.
|
||||
|
||||
Use --dry-run to preview the generated unit files without writing anything.`,
|
||||
RunE: runInstallall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
installallCmd.Flags().BoolVar(&installallDryRun, "dry-run", false, "preview unit files without writing")
|
||||
rootCmd.AddCommand(installallCmd)
|
||||
}
|
||||
|
||||
func runInstallall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := 0
|
||||
skipped := 0
|
||||
removed := 0
|
||||
lingerSeen := map[string]bool{}
|
||||
|
||||
for _, u := range cfg.Units {
|
||||
if !u.Enabled {
|
||||
if ini.IsInstalled(u, prefix, suffix) {
|
||||
if installallDryRun {
|
||||
fmt.Printf(" ~ would remove: %s\n", ini.ServiceName(u, prefix, suffix))
|
||||
} else {
|
||||
if err := ini.Uninstall(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
|
||||
} else {
|
||||
fmt.Printf(" - removed: %s (disabled)\n", ini.ServiceName(u, prefix, suffix))
|
||||
removed++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if installallDryRun {
|
||||
content, err := ini.Generate(u, prefix, suffix)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ %s: %v\n", u.Name, err)
|
||||
continue
|
||||
}
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Printf("\n--- %s ---\n%s\n", path, content)
|
||||
installed++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ini.Install(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
|
||||
} else {
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Printf(" ✓ installed: %s\n", path)
|
||||
lingerHint(ini, lingerSeen, u)
|
||||
installed++
|
||||
}
|
||||
}
|
||||
|
||||
if !installallDryRun {
|
||||
fmt.Printf("\nDone. Installed: %d Removed: %d Skipped: %d\n", installed, removed, skipped)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1
-9
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -31,14 +30,7 @@ func runList(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
units := make([]config.Unit, len(cfg.Units))
|
||||
copy(units, cfg.Units)
|
||||
sort.Slice(units, func(i, j int) bool {
|
||||
if units[i].Order != units[j].Order {
|
||||
return units[i].Order < units[j].Order
|
||||
}
|
||||
return units[i].Name < units[j].Name
|
||||
})
|
||||
units := sortUnits(cfg.Units, false)
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/warkanum/unitdore/config"
|
||||
)
|
||||
|
||||
// sortUnits returns a copy of units sorted by order then name; reverse flips both.
|
||||
func sortUnits(units []config.Unit, reverse bool) []config.Unit {
|
||||
out := make([]config.Unit, len(units))
|
||||
copy(out, units)
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Order != out[j].Order {
|
||||
return (out[i].Order < out[j].Order) != reverse
|
||||
}
|
||||
return (out[i].Name < out[j].Name) != reverse
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// groupByOrder splits order-sorted units into groups sharing the same order.
|
||||
func groupByOrder(sorted []config.Unit) [][]config.Unit {
|
||||
var groups [][]config.Unit
|
||||
for _, u := range sorted {
|
||||
n := len(groups)
|
||||
if n > 0 && groups[n-1][0].Order == u.Order {
|
||||
groups[n-1] = append(groups[n-1], u)
|
||||
continue
|
||||
}
|
||||
groups = append(groups, []config.Unit{u})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// runOrdered runs fn for every unit: groups of equal order one after another,
|
||||
// units within a group in parallel. Start delays live in the generated service
|
||||
// files, so a blocking start covers them. Output is printed per group in name
|
||||
// order. Returns the number of units fn succeeded and failed for.
|
||||
func runOrdered(units []config.Unit, w io.Writer, fn func(u config.Unit) (string, error)) (ok, failed int) {
|
||||
for _, g := range groupByOrder(sortUnits(units, false)) {
|
||||
msgs := make([]string, len(g))
|
||||
errs := make([]error, len(g))
|
||||
var wg sync.WaitGroup
|
||||
for j, u := range g {
|
||||
wg.Add(1)
|
||||
go func(j int, u config.Unit) {
|
||||
defer wg.Done()
|
||||
msgs[j], errs[j] = fn(u)
|
||||
}(j, u)
|
||||
}
|
||||
wg.Wait()
|
||||
for j, u := range g {
|
||||
if errs[j] != nil {
|
||||
fmt.Fprintf(w, " ✗ failed: %s: %v\n", u.Name, errs[j])
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ %s\n", msgs[j])
|
||||
ok++
|
||||
}
|
||||
}
|
||||
return ok, failed
|
||||
}
|
||||
+29
-26
@@ -2,49 +2,52 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var startAll bool
|
||||
|
||||
var startCmd = &cobra.Command{
|
||||
Use: "start <name>",
|
||||
Short: "Start a unit by name",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runStart,
|
||||
Use: "start <name> | --all",
|
||||
Short: "Start a unit by name (or all with --all)",
|
||||
Long: `Start starts the named installed unit without enabling it at boot.
|
||||
|
||||
With --all (same as 'startall') it starts every enabled, installed unit in
|
||||
startup order: equal 'order' values in parallel, groups one after another.
|
||||
'delay' is waited inside the generated service.`,
|
||||
Args: nameOrAll(&startAll),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, ini, err := loadInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if startAll {
|
||||
bulkStart(cfg, ini, os.Stdout, initsys.Init.Start, "started")
|
||||
return nil
|
||||
}
|
||||
return startOne(cfg, ini, args[0], os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
startCmd.Flags().BoolVar(&startAll, "all", false, "start all enabled, installed units")
|
||||
rootCmd.AddCommand(startCmd)
|
||||
}
|
||||
|
||||
func runStart(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
func startOne(cfg *config.Config, ini initsys.Init, name string, w io.Writer) error {
|
||||
u, err := installedUnit(cfg, ini, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, " ▶ starting %s...\n", ini.ServiceName(*u, cfg.Prefix, cfg.Suffix))
|
||||
if err := ini.Start(*u, cfg.Prefix, cfg.Suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ini.IsInstalled(*u, prefix, suffix) {
|
||||
return fmt.Errorf("%s is not installed — run 'unitdore install' first", name)
|
||||
}
|
||||
|
||||
fmt.Printf(" ▶ starting %s...\n", ini.ServiceName(*u, prefix, suffix))
|
||||
if err := ini.Start(*u, prefix, suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" ✓ started: %s\n", name)
|
||||
fmt.Fprintf(w, " ✓ started: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var startallCmd = &cobra.Command{
|
||||
Use: "startall",
|
||||
Short: "Start all installed, enabled units",
|
||||
Long: `Startall runs 'systemctl start' for all enabled units that have been installed.
|
||||
Units must be installed first via 'unitdore installall'.`,
|
||||
RunE: runStartall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(startallCmd)
|
||||
}
|
||||
|
||||
func runStartall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
units := make([]config.Unit, len(cfg.Units))
|
||||
copy(units, cfg.Units)
|
||||
sort.Slice(units, func(i, j int) bool {
|
||||
if units[i].Order != units[j].Order {
|
||||
return units[i].Order < units[j].Order
|
||||
}
|
||||
return units[i].Name < units[j].Name
|
||||
})
|
||||
|
||||
started := 0
|
||||
failed := 0
|
||||
|
||||
for _, u := range units {
|
||||
if !u.Enabled {
|
||||
continue
|
||||
}
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
fmt.Printf(" ! %s: not installed — run 'unitdore install' first\n", u.Name)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ▶ starting: %s...\n", ini.ServiceName(u, prefix, suffix))
|
||||
if err := ini.Start(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Printf(" ✓ started: %s\n", u.Name)
|
||||
started++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nDone. Started: %d Failed: %d\n", started, failed)
|
||||
return nil
|
||||
}
|
||||
+1
-9
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -47,14 +46,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
units := make([]config.Unit, len(cfg.Units))
|
||||
copy(units, cfg.Units)
|
||||
sort.Slice(units, func(i, j int) bool {
|
||||
if units[i].Order != units[j].Order {
|
||||
return units[i].Order < units[j].Order
|
||||
}
|
||||
return units[i].Name < units[j].Name
|
||||
})
|
||||
units := sortUnits(cfg.Units, false)
|
||||
|
||||
rows := make([]statusRow, 0, len(units))
|
||||
for _, u := range units {
|
||||
|
||||
+28
-26
@@ -2,49 +2,51 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var stopAllFlag bool
|
||||
|
||||
var stopCmd = &cobra.Command{
|
||||
Use: "stop <name>",
|
||||
Short: "Stop a unit by name",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runStop,
|
||||
Use: "stop <name> | --all",
|
||||
Short: "Stop a unit by name (or all with --all)",
|
||||
Long: `Stop stops the named installed unit.
|
||||
|
||||
With --all (same as 'stopall') it stops every installed unit in reverse
|
||||
startup order, one at a time.`,
|
||||
Args: nameOrAll(&stopAllFlag),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, ini, err := loadInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stopAllFlag {
|
||||
stopAll(cfg, ini, os.Stdout)
|
||||
return nil
|
||||
}
|
||||
return stopOne(cfg, ini, args[0], os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
stopCmd.Flags().BoolVar(&stopAllFlag, "all", false, "stop all installed units")
|
||||
rootCmd.AddCommand(stopCmd)
|
||||
}
|
||||
|
||||
func runStop(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
func stopOne(cfg *config.Config, ini initsys.Init, name string, w io.Writer) error {
|
||||
u, err := installedUnit(cfg, ini, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, " ■ stopping %s...\n", ini.ServiceName(*u, cfg.Prefix, cfg.Suffix))
|
||||
if err := ini.Stop(*u, cfg.Prefix, cfg.Suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ini.IsInstalled(*u, prefix, suffix) {
|
||||
return fmt.Errorf("%s is not installed", name)
|
||||
}
|
||||
|
||||
fmt.Printf(" ■ stopping %s...\n", ini.ServiceName(*u, prefix, suffix))
|
||||
if err := ini.Stop(*u, prefix, suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" ✓ stopped: %s\n", name)
|
||||
fmt.Fprintf(w, " ✓ stopped: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var stopallCmd = &cobra.Command{
|
||||
Use: "stopall",
|
||||
Short: "Stop all installed units",
|
||||
Long: `Stopall runs 'systemctl stop' for all installed units in reverse startup order.`,
|
||||
RunE: runStopall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(stopallCmd)
|
||||
}
|
||||
|
||||
func runStopall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reverse order for shutdown
|
||||
units := make([]config.Unit, len(cfg.Units))
|
||||
copy(units, cfg.Units)
|
||||
sort.Slice(units, func(i, j int) bool {
|
||||
if units[i].Order != units[j].Order {
|
||||
return units[i].Order > units[j].Order
|
||||
}
|
||||
return units[i].Name > units[j].Name
|
||||
})
|
||||
|
||||
stopped := 0
|
||||
failed := 0
|
||||
|
||||
for _, u := range units {
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ■ stopping: %s...\n", ini.ServiceName(u, prefix, suffix))
|
||||
if err := ini.Stop(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed: %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Printf(" ✓ stopped: %s\n", u.Name)
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nDone. Stopped: %d Failed: %d\n", stopped, failed)
|
||||
return nil
|
||||
}
|
||||
+28
-28
@@ -2,58 +2,58 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var uninstallAllFlag bool
|
||||
|
||||
var uninstallCmd = &cobra.Command{
|
||||
Use: "uninstall <name>",
|
||||
Short: "Disable, stop, and remove the service file for a unit",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runUninstall,
|
||||
Use: "uninstall <name> | --all",
|
||||
Short: "Stop, disable and remove the service file for a unit (or all with --all)",
|
||||
Long: `Uninstall stops, disables and removes the service file of the named unit.
|
||||
|
||||
With --all (same as 'uninstallall') it does so for every installed unit in
|
||||
reverse startup order.`,
|
||||
Args: nameOrAll(&uninstallAllFlag),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, ini, err := loadInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uninstallAllFlag {
|
||||
uninstallAll(cfg, ini, os.Stdout)
|
||||
return nil
|
||||
}
|
||||
return uninstallOne(cfg, ini, args[0], os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
uninstallCmd.Flags().BoolVar(&uninstallAllFlag, "all", false, "uninstall all installed units")
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
}
|
||||
|
||||
func runUninstall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
func uninstallOne(cfg *config.Config, ini initsys.Init, name string, w io.Writer) error {
|
||||
u, err := installedUnit(cfg, ini, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ini.IsInstalled(*u, prefix, suffix) {
|
||||
return fmt.Errorf("%s is not installed", name)
|
||||
}
|
||||
|
||||
svcName := ini.ServiceName(*u, prefix, suffix)
|
||||
|
||||
fmt.Printf(" ■ disabling %s...\n", svcName)
|
||||
fmt.Fprintf(w, " ■ disabling %s...\n", ini.ServiceName(*u, prefix, suffix))
|
||||
if err := ini.Disable(*u, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("disabling unit: %w", err)
|
||||
}
|
||||
|
||||
path, _ := ini.UnitPath(*u, prefix, suffix)
|
||||
fmt.Printf(" ✗ removing %s...\n", path)
|
||||
fmt.Fprintf(w, " ✗ removing %s...\n", path)
|
||||
if err := ini.Uninstall(*u, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("removing service file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ uninstalled: %s\n", name)
|
||||
fmt.Fprintf(w, " ✓ uninstalled: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
"github.com/warkanum/unitdore/initsys"
|
||||
)
|
||||
|
||||
var uninstallallCmd = &cobra.Command{
|
||||
Use: "uninstallall",
|
||||
Short: "Stop, disable, and remove service files for all installed units",
|
||||
Long: `Uninstallall runs 'systemctl disable --now' and removes the .service file for every installed unit, in reverse startup order.`,
|
||||
RunE: runUninstallall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(uninstallallCmd)
|
||||
}
|
||||
|
||||
func runUninstallall(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
units := make([]config.Unit, len(cfg.Units))
|
||||
copy(units, cfg.Units)
|
||||
sort.Slice(units, func(i, j int) bool {
|
||||
if units[i].Order != units[j].Order {
|
||||
return units[i].Order > units[j].Order
|
||||
}
|
||||
return units[i].Name > units[j].Name
|
||||
})
|
||||
|
||||
removed := 0
|
||||
failed := 0
|
||||
|
||||
for _, u := range units {
|
||||
if !ini.IsInstalled(u, prefix, suffix) {
|
||||
continue
|
||||
}
|
||||
svcName := ini.ServiceName(u, prefix, suffix)
|
||||
fmt.Printf(" ■ disabling %s...\n", svcName)
|
||||
if err := ini.Disable(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed to disable %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
path, _ := ini.UnitPath(u, prefix, suffix)
|
||||
fmt.Printf(" ✗ removing %s...\n", path)
|
||||
if err := ini.Uninstall(u, prefix, suffix); err != nil {
|
||||
fmt.Printf(" ✗ failed to remove %s: %v\n", u.Name, err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ✓ uninstalled: %s\n", u.Name)
|
||||
removed++
|
||||
}
|
||||
|
||||
fmt.Printf("\nDone. Uninstalled: %d Failed: %d\n", removed, failed)
|
||||
return nil
|
||||
}
|
||||
+63
-32
@@ -3,6 +3,8 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warkanum/unitdore/config"
|
||||
@@ -38,44 +40,73 @@ func runUpdate(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("order") {
|
||||
u.Order = updateOrder
|
||||
fmt.Printf(" order → %d\n", u.Order)
|
||||
}
|
||||
if updateEnable {
|
||||
u.Enabled = true
|
||||
u.DisabledReason = ""
|
||||
fmt.Printf(" enabled\n")
|
||||
}
|
||||
if updateDisable {
|
||||
u.Enabled = false
|
||||
fmt.Printf(" disabled\n")
|
||||
}
|
||||
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
ini, err := initsys.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ini.IsInstalled(*u, prefix, suffix) {
|
||||
if err := ini.Install(*u, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("recreating service file: %w", err)
|
||||
}
|
||||
path, _ := ini.UnitPath(*u, prefix, suffix)
|
||||
fmt.Printf(" ✓ service file recreated: %s\n", path)
|
||||
var order *int
|
||||
if cmd.Flags().Changed("order") {
|
||||
order = &updateOrder
|
||||
}
|
||||
return applyUpdate(cfg, ini, args[0], order, updateEnable, updateDisable, configPath, os.Stdout)
|
||||
}
|
||||
|
||||
// applyUpdate changes a unit's config, saves it and brings the installed
|
||||
// service file in line: re-created if enabled, stopped and removed if disabled.
|
||||
func applyUpdate(cfg *config.Config, ini initsys.Init, name string, order *int, enable, disable bool, path string, w io.Writer) error {
|
||||
u := cfg.FindUnit(name)
|
||||
if u == nil {
|
||||
return fmt.Errorf("unit %q not found", name)
|
||||
}
|
||||
|
||||
if order != nil {
|
||||
u.Order = *order
|
||||
fmt.Fprintf(w, " order → %d\n", u.Order)
|
||||
}
|
||||
if enable {
|
||||
u.Enabled = true
|
||||
u.DisabledReason = ""
|
||||
fmt.Fprintf(w, " enabled\n")
|
||||
}
|
||||
if disable {
|
||||
u.Enabled = false
|
||||
fmt.Fprintf(w, " disabled\n")
|
||||
}
|
||||
|
||||
if err := cfg.ResolveOrder(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cfg.Save(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix, suffix := cfg.Prefix, cfg.Suffix
|
||||
|
||||
// The target is stopped and removed if now disabled
|
||||
if ini.IsInstalled(*u, prefix, suffix) && !u.Enabled {
|
||||
svcPath, _ := ini.UnitPath(*u, prefix, suffix)
|
||||
if err := ini.Disable(*u, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("disabling unit: %w", err)
|
||||
}
|
||||
if err := ini.Uninstall(*u, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("removing service file: %w", err)
|
||||
}
|
||||
fmt.Fprintf(w, " ✓ service stopped and removed: %s\n", svcPath)
|
||||
}
|
||||
|
||||
// Order and enabled state change the After=/delay of other units too, so
|
||||
// every installed, enabled unit is regenerated
|
||||
for i := range cfg.Units {
|
||||
o := cfg.Units[i]
|
||||
if !o.Enabled || !ini.IsInstalled(o, prefix, suffix) {
|
||||
continue
|
||||
}
|
||||
if err := ini.Install(o, prefix, suffix); err != nil {
|
||||
return fmt.Errorf("recreating service file for %s: %w", o.Name, err)
|
||||
}
|
||||
svcPath, _ := ini.UnitPath(o, prefix, suffix)
|
||||
fmt.Fprintf(w, " ✓ service file recreated: %s\n", svcPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user