290 lines
8.2 KiB
Go
290 lines
8.2 KiB
Go
//go:build linux
|
||
|
||
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"strings"
|
||
)
|
||
|
||
// NewServiceManager 创建服务管理器 (Linux)
|
||
func NewServiceManager() ServiceManager {
|
||
return &LinuxServiceManager{}
|
||
}
|
||
|
||
// LinuxServiceManager Linux systemd服务管理器
|
||
type LinuxServiceManager struct{}
|
||
|
||
func (m *LinuxServiceManager) Install(execPath string) error {
|
||
// 检查是否有足够权限操作系统服务
|
||
if err := m.checkPermissions(); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := ensureLogDir(); err != nil {
|
||
return fmt.Errorf("创建日志目录失败: %v", err)
|
||
}
|
||
|
||
serviceName := getServiceName()
|
||
serviceContent := m.generateSystemdUnit(execPath)
|
||
// 写入systemd服务文件
|
||
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", strings.ToLower(serviceName))
|
||
if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
|
||
return fmt.Errorf("写入服务文件失败: %v", err)
|
||
}
|
||
|
||
// 处理 SELinux 上下文,确保可执行文件可以被 systemd 执行
|
||
if err := m.handleSELinuxContext(execPath); err != nil {
|
||
// SELinux 处理失败不算致命错误,只记录警告
|
||
fmt.Printf("警告: SELinux 上下文设置失败: %v\n", err)
|
||
}
|
||
|
||
// 重新加载systemd配置
|
||
cmd := exec.Command("systemctl", "daemon-reload")
|
||
if output, err := cmd.CombinedOutput(); err != nil {
|
||
return fmt.Errorf("重新加载systemd配置失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
// 启用服务自动启动
|
||
cmd = exec.Command("systemctl", "enable", strings.ToLower(serviceName))
|
||
if output, err := cmd.CombinedOutput(); err != nil {
|
||
return fmt.Errorf("启用服务自动启动失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (m *LinuxServiceManager) Uninstall() error {
|
||
serviceName := strings.ToLower(getServiceName())
|
||
|
||
// 停止服务
|
||
m.Stop()
|
||
|
||
// 禁用服务
|
||
cmd := exec.Command("systemctl", "disable", serviceName)
|
||
cmd.CombinedOutput() // 忽略错误
|
||
|
||
// 删除服务文件
|
||
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", serviceName)
|
||
if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) {
|
||
return fmt.Errorf("删除服务文件失败: %v", err)
|
||
}
|
||
|
||
// 重新加载systemd配置
|
||
cmd = exec.Command("systemctl", "daemon-reload")
|
||
cmd.CombinedOutput() // 忽略错误
|
||
|
||
return nil
|
||
}
|
||
|
||
func (m *LinuxServiceManager) Start() error {
|
||
serviceName := strings.ToLower(getServiceName())
|
||
|
||
cmd := exec.Command("systemctl", "start", serviceName)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("启动服务失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (m *LinuxServiceManager) Stop() error {
|
||
serviceName := strings.ToLower(getServiceName())
|
||
|
||
cmd := exec.Command("systemctl", "stop", serviceName)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("停止服务失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (m *LinuxServiceManager) Restart() error {
|
||
serviceName := strings.ToLower(getServiceName())
|
||
|
||
cmd := exec.Command("systemctl", "restart", serviceName)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("重启服务失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (m *LinuxServiceManager) Status() (ServiceStatus, error) {
|
||
serviceName := strings.ToLower(getServiceName())
|
||
|
||
// 检查服务是否存在
|
||
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", serviceName)
|
||
if _, err := os.Stat(servicePath); os.IsNotExist(err) {
|
||
return StatusNotInstalled, nil
|
||
}
|
||
cmd := exec.Command("systemctl", "is-active", serviceName)
|
||
output, err := cmd.CombinedOutput()
|
||
outputStr := strings.TrimSpace(string(output))
|
||
|
||
// 处理不同的退出状态
|
||
if err != nil {
|
||
if exitError, ok := err.(*exec.ExitError); ok {
|
||
// systemctl is-active 的退出码含义:
|
||
// 0: active, 1: inactive, 3: activating/deactivating
|
||
switch exitError.ExitCode() {
|
||
case 1:
|
||
// 服务未运行
|
||
switch outputStr {
|
||
case "inactive":
|
||
return StatusStopped, nil
|
||
case "failed":
|
||
return StatusStopped, nil
|
||
default:
|
||
return StatusStopped, nil
|
||
}
|
||
case 3:
|
||
// 服务正在启动或停止中
|
||
switch outputStr {
|
||
case "activating":
|
||
return StatusRunning, nil // 启动中视为运行状态
|
||
case "deactivating":
|
||
return StatusStopped, nil // 停止中视为停止状态
|
||
default:
|
||
return StatusStopped, nil // 其他转换状态默认为停止
|
||
}
|
||
default:
|
||
if strings.Contains(outputStr, "could not be found") {
|
||
return StatusNotInstalled, nil
|
||
}
|
||
return StatusUnknown, fmt.Errorf("获取服务状态失败: %v, 输出: %s", err, outputStr)
|
||
}
|
||
} else {
|
||
return StatusUnknown, fmt.Errorf("执行systemctl命令失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// 没有错误的情况下,服务应该是active状态
|
||
switch outputStr {
|
||
case "active":
|
||
return StatusRunning, nil
|
||
case "activating":
|
||
return StatusRunning, nil // 正在启动中,视为运行状态
|
||
case "inactive":
|
||
return StatusStopped, nil
|
||
case "failed":
|
||
return StatusStopped, nil
|
||
case "deactivating":
|
||
return StatusStopped, nil // 正在停止中,视为停止状态
|
||
default:
|
||
return StatusUnknown, fmt.Errorf("未知的服务状态: %s", outputStr)
|
||
}
|
||
}
|
||
|
||
func (m *LinuxServiceManager) generateSystemdUnit(execPath string) string {
|
||
description := getServiceDescription()
|
||
|
||
// 获取用户配置目录
|
||
userConfigDir, err := getUserConfigDir()
|
||
if err != nil {
|
||
// 如果获取失败,使用默认路径
|
||
userConfigDir = "/root/.hostsync"
|
||
}
|
||
return fmt.Sprintf(`[Unit]
|
||
Description=%s
|
||
After=network.target network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=%s service run -c %s
|
||
Restart=always
|
||
RestartSec=10
|
||
User=root
|
||
Group=root
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=hostsync
|
||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
`, description, execPath, userConfigDir)
|
||
}
|
||
|
||
// checkPermissions 检查是否有足够权限操作系统服务
|
||
func (m *LinuxServiceManager) checkPermissions() error {
|
||
// 检查是否为 root 用户
|
||
if os.Getuid() != 0 {
|
||
return fmt.Errorf("安装系统服务需要 root 权限,请使用 sudo 运行")
|
||
}
|
||
|
||
// 检查 /etc/systemd/system 目录是否可写
|
||
testPath := "/etc/systemd/system"
|
||
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||
return fmt.Errorf("systemd 系统目录不存在: %s", testPath)
|
||
}
|
||
|
||
// 尝试创建一个测试文件来验证写权限
|
||
testFile := fmt.Sprintf("%s/.hostsync-permission-test", testPath)
|
||
if file, err := os.Create(testFile); err != nil {
|
||
return fmt.Errorf("无法写入系统服务目录 %s: %v", testPath, err)
|
||
} else {
|
||
file.Close()
|
||
os.Remove(testFile) // 清理测试文件
|
||
}
|
||
|
||
// 检查 systemctl 命令是否可用
|
||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||
return fmt.Errorf("系统未安装 systemd 或 systemctl 命令不可用: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// handleSELinuxContext 处理 SELinux 上下文设置
|
||
func (m *LinuxServiceManager) handleSELinuxContext(execPath string) error {
|
||
// 检查是否启用了 SELinux
|
||
if !m.isSELinuxEnabled() {
|
||
return nil // SELinux 未启用,无需处理
|
||
}
|
||
|
||
// 检查 chcon 命令是否可用
|
||
if _, err := exec.LookPath("chcon"); err != nil {
|
||
return fmt.Errorf("chcon 命令不可用: %v", err)
|
||
}
|
||
|
||
// 为可执行文件设置正确的 SELinux 上下文
|
||
// bin_t 类型允许程序被 systemd 执行
|
||
cmd := exec.Command("chcon", "-t", "bin_t", execPath)
|
||
if output, err := cmd.CombinedOutput(); err != nil {
|
||
return fmt.Errorf("设置 SELinux 上下文失败: %v, 输出: %s", err, string(output))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// isSELinuxEnabled 检查 SELinux 是否启用
|
||
func (m *LinuxServiceManager) isSELinuxEnabled() bool {
|
||
// 检查 /selinux/enforce 文件是否存在
|
||
if _, err := os.Stat("/selinux/enforce"); err == nil {
|
||
return true
|
||
}
|
||
|
||
// 检查 /sys/fs/selinux/enforce 文件是否存在
|
||
if _, err := os.Stat("/sys/fs/selinux/enforce"); err == nil {
|
||
return true
|
||
}
|
||
|
||
// 尝试运行 getenforce 命令
|
||
if _, err := exec.LookPath("getenforce"); err == nil {
|
||
cmd := exec.Command("getenforce")
|
||
if output, err := cmd.CombinedOutput(); err == nil {
|
||
status := strings.TrimSpace(string(output))
|
||
return status == "Enforcing" || status == "Permissive"
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|