249 lines
6.1 KiB
Go
249 lines
6.1 KiB
Go
//go:build darwin
|
|
|
|
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// NewServiceManager 创建macOS服务管理器实例
|
|
func NewServiceManager() ServiceManager {
|
|
return &DarwinServiceManager{}
|
|
}
|
|
|
|
// DarwinServiceManager macOS LaunchD服务管理器
|
|
type DarwinServiceManager struct{}
|
|
|
|
func (m *DarwinServiceManager) Install(execPath string) error {
|
|
// 检查是否有足够权限操作系统服务
|
|
if err := m.checkPermissions(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ensureLogDir(); err != nil {
|
|
return fmt.Errorf("创建日志目录失败: %v", err)
|
|
}
|
|
|
|
plistContent := m.generateLaunchDPlist(execPath)
|
|
|
|
// 获取plist文件路径
|
|
plistPath, err := m.getPlistPath()
|
|
if err != nil {
|
|
return fmt.Errorf("获取plist路径失败: %v", err)
|
|
}
|
|
|
|
// 创建目录
|
|
if err := os.MkdirAll(filepath.Dir(plistPath), 0755); err != nil {
|
|
return fmt.Errorf("创建plist目录失败: %v", err)
|
|
}
|
|
|
|
// 写入plist文件
|
|
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
|
|
return fmt.Errorf("写入plist文件失败: %v", err)
|
|
}
|
|
|
|
// 加载服务
|
|
cmd := exec.Command("launchctl", "load", plistPath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("加载服务失败: %v, 输出: %s", err, string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DarwinServiceManager) Uninstall() error {
|
|
|
|
// 停止服务
|
|
m.Stop()
|
|
|
|
// 获取plist文件路径
|
|
plistPath, err := m.getPlistPath()
|
|
if err != nil {
|
|
return fmt.Errorf("获取plist路径失败: %v", err)
|
|
}
|
|
|
|
// 卸载服务
|
|
cmd := exec.Command("launchctl", "unload", plistPath)
|
|
cmd.CombinedOutput() // 忽略错误
|
|
|
|
// 删除plist文件
|
|
if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("删除plist文件失败: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DarwinServiceManager) Start() error {
|
|
serviceName := m.getServiceIdentifier()
|
|
|
|
cmd := exec.Command("launchctl", "start", serviceName)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("启动服务失败: %v, 输出: %s", err, string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DarwinServiceManager) Stop() error {
|
|
serviceName := m.getServiceIdentifier()
|
|
|
|
cmd := exec.Command("launchctl", "stop", serviceName)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("停止服务失败: %v, 输出: %s", err, string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *DarwinServiceManager) Restart() error {
|
|
if err := m.Stop(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return m.Start()
|
|
}
|
|
|
|
func (m *DarwinServiceManager) Status() (ServiceStatus, error) {
|
|
serviceName := m.getServiceIdentifier()
|
|
|
|
// 检查plist文件是否存在
|
|
plistPath, err := m.getPlistPath()
|
|
if err != nil {
|
|
return StatusUnknown, err
|
|
}
|
|
|
|
if _, err := os.Stat(plistPath); os.IsNotExist(err) {
|
|
return StatusNotInstalled, nil
|
|
}
|
|
|
|
cmd := exec.Command("launchctl", "list", serviceName)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
// 如果服务不在列表中,可能是未运行
|
|
return StatusStopped, nil
|
|
}
|
|
|
|
// 简单检查输出中是否包含PID
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, serviceName) && !strings.Contains(outputStr, "-") {
|
|
return StatusRunning, nil
|
|
}
|
|
|
|
return StatusStopped, nil
|
|
}
|
|
|
|
func (m *DarwinServiceManager) getServiceIdentifier() string {
|
|
return fmt.Sprintf("com.deepwn.%s", strings.ToLower(getServiceName()))
|
|
}
|
|
|
|
func (m *DarwinServiceManager) getPlistPath() (string, error) {
|
|
serviceName := m.getServiceIdentifier()
|
|
|
|
// 检查是否以root权限运行
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if currentUser.Uid == "0" {
|
|
// 系统级服务
|
|
return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", serviceName), nil
|
|
} else {
|
|
// 用户级服务
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", homeDir, serviceName), nil
|
|
}
|
|
}
|
|
|
|
func (m *DarwinServiceManager) generateLaunchDPlist(execPath string) string {
|
|
serviceName := m.getServiceIdentifier()
|
|
logPath := getLogPath()
|
|
|
|
// 获取用户配置目录
|
|
userConfigDir, err := getUserConfigDir()
|
|
if err != nil {
|
|
// 如果获取失败,使用默认路径
|
|
currentUser, _ := user.Current()
|
|
if currentUser != nil {
|
|
userConfigDir = filepath.Join(currentUser.HomeDir, ".hostsync")
|
|
} else {
|
|
userConfigDir = "/Users/Shared/.hostsync"
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>%s</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>%s</string>
|
|
<string>service</string>
|
|
<string>run</string>
|
|
<string>--config</string>
|
|
<string>%s</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>%s</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
</plist>
|
|
`, serviceName, execPath, userConfigDir, logPath, logPath)
|
|
}
|
|
|
|
// checkPermissions 检查是否有足够权限操作系统服务
|
|
func (m *DarwinServiceManager) checkPermissions() error {
|
|
// 检查是否为 root 用户
|
|
if os.Getuid() != 0 {
|
|
return fmt.Errorf("安装系统服务需要 root 权限,请使用 sudo 运行")
|
|
}
|
|
|
|
// 检查 launchctl 命令是否可用
|
|
if _, err := exec.LookPath("launchctl"); err != nil {
|
|
return fmt.Errorf("launchctl 命令不可用: %v", err)
|
|
}
|
|
|
|
// 获取 plist 文件路径并检查目录权限
|
|
plistPath, err := m.getPlistPath()
|
|
if err != nil {
|
|
return fmt.Errorf("获取 plist 路径失败: %v", err)
|
|
}
|
|
|
|
plistDir := filepath.Dir(plistPath)
|
|
// 检查目录是否存在,如果不存在尝试创建来测试权限
|
|
if _, err := os.Stat(plistDir); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(plistDir, 0755); err != nil {
|
|
return fmt.Errorf("无法创建 plist 目录 %s: %v", plistDir, err)
|
|
}
|
|
}
|
|
|
|
// 尝试创建一个测试文件来验证写权限
|
|
testFile := filepath.Join(plistDir, ".hostsync-permission-test")
|
|
if file, err := os.Create(testFile); err != nil {
|
|
return fmt.Errorf("无法写入 plist 目录 %s: %v", plistDir, err)
|
|
} else {
|
|
file.Close()
|
|
os.Remove(testFile) // 清理测试文件
|
|
}
|
|
|
|
return nil
|
|
}
|