f536987a7e
- doc: add comment
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
package systemd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// loadStateProperty systemctl show 输出的加载状态属性名。
|
|
loadStateProperty = "LoadState"
|
|
// activeStateProperty systemctl show 输出的活动状态属性名。
|
|
activeStateProperty = "ActiveState"
|
|
// subStateProperty systemctl show 输出的子状态属性名。
|
|
subStateProperty = "SubState"
|
|
// loadedState 表示单元已加载。
|
|
loadedState = "loaded"
|
|
// notFoundState 表示单元未找到。
|
|
notFoundState = "not-found"
|
|
)
|
|
|
|
// Systemctl 直接调用一个精确的 systemctl 可执行文件,绝不经过 shell。
|
|
type Systemctl struct {
|
|
// executable systemctl 的绝对路径。
|
|
executable string
|
|
}
|
|
|
|
// NewSystemctl 要求传入本地守护进程配置提供的可执行文件绝对路径。
|
|
func NewSystemctl(executable string) (*Systemctl, error) {
|
|
if !filepath.IsAbs(executable) {
|
|
return nil, errors.New("systemctl executable path must be absolute")
|
|
}
|
|
return &Systemctl{executable: executable}, nil
|
|
}
|
|
|
|
// Inspect 查询指定单元的关键状态属性并返回其状态。
|
|
// 未找到单元时错误包装 ErrUnitNotFound;加载状态异常时返回错误。
|
|
func (s *Systemctl) Inspect(ctx context.Context, unitName string) (Unit, error) {
|
|
if err := validateUnitName(unitName); err != nil {
|
|
return Unit{}, err
|
|
}
|
|
command := exec.CommandContext(ctx, s.executable,
|
|
"show",
|
|
"--no-pager",
|
|
"--property="+loadStateProperty,
|
|
"--property="+activeStateProperty,
|
|
"--property="+subStateProperty,
|
|
"--",
|
|
unitName,
|
|
)
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
return Unit{}, commandError("inspect systemd unit "+unitName, output, err)
|
|
}
|
|
unit, err := parseUnitProperties(unitName, output)
|
|
if err != nil {
|
|
return Unit{}, err
|
|
}
|
|
if unit.LoadState == notFoundState {
|
|
return Unit{}, fmt.Errorf("inspect systemd unit %s: %w", unitName, ErrUnitNotFound)
|
|
}
|
|
if unit.LoadState != loadedState {
|
|
return Unit{}, fmt.Errorf("systemd unit %s has unsupported load state %q", unitName, unit.LoadState)
|
|
}
|
|
return unit, nil
|
|
}
|
|
|
|
// Start 启动指定单元。
|
|
func (s *Systemctl) Start(ctx context.Context, unitName string) error {
|
|
return s.changeState(ctx, "start", unitName)
|
|
}
|
|
|
|
// Stop 停止指定单元。
|
|
func (s *Systemctl) Stop(ctx context.Context, unitName string) error {
|
|
return s.changeState(ctx, "stop", unitName)
|
|
}
|
|
|
|
// changeState 通过 systemctl 执行给定动作(start 或 stop)。
|
|
func (s *Systemctl) changeState(ctx context.Context, action, unitName string) error {
|
|
if err := validateUnitName(unitName); err != nil {
|
|
return err
|
|
}
|
|
output, err := exec.CommandContext(ctx, s.executable, action, "--", unitName).CombinedOutput()
|
|
if err != nil {
|
|
return commandError(action+" systemd unit "+unitName, output, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseUnitProperties 解析 systemctl show 的输出,
|
|
// 严格要求恰好包含三个预期属性且不重复、不多余。
|
|
func parseUnitProperties(unitName string, output []byte) (Unit, error) {
|
|
values := make(map[string]string, 3)
|
|
scanner := bufio.NewScanner(bytes.NewReader(output))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
key, value, found := strings.Cut(line, "=")
|
|
if !found {
|
|
return Unit{}, fmt.Errorf("decode systemd unit %s property line %q", unitName, line)
|
|
}
|
|
switch key {
|
|
case loadStateProperty, activeStateProperty, subStateProperty:
|
|
if _, duplicate := values[key]; duplicate {
|
|
return Unit{}, fmt.Errorf("decode systemd unit %s duplicate property %s", unitName, key)
|
|
}
|
|
values[key] = value
|
|
default:
|
|
return Unit{}, fmt.Errorf("decode systemd unit %s unexpected property %q", unitName, key)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return Unit{}, fmt.Errorf("decode systemd unit %s properties: %w", unitName, err)
|
|
}
|
|
for _, key := range []string{loadStateProperty, activeStateProperty, subStateProperty} {
|
|
if _, found := values[key]; !found {
|
|
return Unit{}, fmt.Errorf("decode systemd unit %s missing property %s", unitName, key)
|
|
}
|
|
}
|
|
return Unit{
|
|
Name: unitName,
|
|
LoadState: values[loadStateProperty],
|
|
ActiveState: values[activeStateProperty],
|
|
SubState: values[subStateProperty],
|
|
}, nil
|
|
}
|
|
|
|
// validateUnitName 校验单元名称必须非空且不含首尾空白。
|
|
func validateUnitName(unitName string) error {
|
|
if unitName == "" || strings.TrimSpace(unitName) != unitName {
|
|
return errors.New("exact systemd unit name is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// commandError 包装命令执行错误,并附带去除首尾空白的命令输出作为诊断细节。
|
|
func commandError(action string, output []byte, err error) error {
|
|
detail := strings.TrimSpace(string(output))
|
|
if detail == "" {
|
|
return fmt.Errorf("%s: %w", action, err)
|
|
}
|
|
return fmt.Errorf("%s: %w: %s", action, err, detail)
|
|
}
|
|
|
|
var _ Manager = (*Systemctl)(nil)
|