From 9bb52164edc4703ed1f4dcda01feafea5b7f264e Mon Sep 17 00:00:00 2001 From: xxnuo Date: Fri, 12 Jan 2024 01:24:36 +0800 Subject: [PATCH] Basic support system service mode todo: Log redirection and parameter forwarding --- go.mod | 1 + go.sum | 3 ++ main.go | 25 ++++++++++++--- service/control.go | 30 +++++++++++++++++ service/parser.go | 69 ++++++++++++++++++++++++++++++++++++++++ service/privilege.go | 36 +++++++++++++++++++++ service/service.go | 76 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 service/control.go create mode 100644 service/parser.go create mode 100644 service/privilege.go create mode 100644 service/service.go diff --git a/go.mod b/go.mod index f8252128..892e027c 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,7 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/kardianos/service v1.2.2 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index c3209df8..94b2e3cc 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= +github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= @@ -246,6 +248,7 @@ golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 748fa2e3..3db87a79 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "github.com/metacubex/mihomo/service" "os" "os/signal" "path/filepath" @@ -23,9 +24,12 @@ import ( ) var ( - version bool - testConfig bool - geodataMode bool + version bool + testConfig bool + geodataMode bool + + serviceMode string + homeDir string configFile string externalUI string @@ -44,11 +48,18 @@ func init() { flag.BoolVar(&geodataMode, "m", false, "set geodata mode") flag.BoolVar(&version, "v", false, "show current version of mihomo") flag.BoolVar(&testConfig, "t", false, "test configuration and exit") + flag.StringVar(&serviceMode, "service", "run", "control mihomo service, available options: [run |install |uninstall | start | stop | restart | status]") + flag.Parse() } func main() { + service.Parser(serviceMode, RealMain) +} + +func RealMain() { _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) @@ -56,7 +67,7 @@ func main() { fmt.Printf("Use tags: %s\n", strings.Join(tags, ", ")) } - return + os.Exit(0) } if homeDir != "" { @@ -85,6 +96,10 @@ func main() { log.Fatalln("Initial configuration directory error: %s", err.Error()) } + if service.Noninteractive { + service.SetLogger(C.Path.HomeDir()) + } + if testConfig { if _, err := executor.Parse(); err != nil { log.Errorln(err.Error()) @@ -92,7 +107,7 @@ func main() { os.Exit(1) } fmt.Printf("configuration file %s test is successful\n", C.Path.Config()) - return + os.Exit(0) } var options []hub.Option diff --git a/service/control.go b/service/control.go new file mode 100644 index 00000000..638d275b --- /dev/null +++ b/service/control.go @@ -0,0 +1,30 @@ +package service + +import ( + "github.com/kardianos/service" + "github.com/metacubex/mihomo/hub/executor" + "os" +) + +func (p *program) Start(s service.Service) error { + // Start should not block. Do the actual work async. + go mainFunc() + return nil +} + +// +//func (p *program) run() { +// // Do work here +// +//} + +func (p *program) Stop(s service.Service) error { + // Stop should not block. Return with a few seconds. + go p.exit() + return nil +} + +func (p *program) exit() { + executor.Shutdown() + os.Exit(0) +} diff --git a/service/parser.go b/service/parser.go new file mode 100644 index 00000000..afa2039c --- /dev/null +++ b/service/parser.go @@ -0,0 +1,69 @@ +package service + +import ( + "github.com/kardianos/service" + "github.com/metacubex/mihomo/log" + "os" +) + +// Parser parses the command line arguments and runs as a service. +// 如果需要程序退出(包括对服务的操作:install、uninstall、start、stop)则返回真 +// 不需要退出程序(run、noninteractive(服务中启动))返回假 +// 其他命令则以 cmd == "run" 处理 +// 注意:本函数需要处理的非常快(毫秒级别) +// 代码以空间换时间 +func Parser(cmd string, runFunc func()) { + status, err := Init(runFunc) + + switch cmd { + case "install": + err = SysService.Install() + + case "uninstall": + err = SysService.Uninstall() + + case "start": + if status != service.StatusRunning { + err = SysService.Start() + } + + case "stop": + if status != service.StatusStopped { + err = SysService.Stop() + } + + case "restart": + err = SysService.Restart() + + case "status": + if err != nil { + log.Errorln("Failed to get service status: %s", err) + return + } + log.Infoln("Service status:%d (0:Unknown 1:Running 2:Stopped)", status) + + case "noninteractive": + // 在服务中启动 + Noninteractive = true + log.Infoln("Running in service.") + err = SysService.Run() + if err != nil { + log.Errorln("Failed to run service: %s", err) + } + default: + // 直接运行 + log.Infoln("Running in terminal.") + err = SysService.Run() + if err != nil { + log.Errorln("Failed to run service: %s", err) + } + } + + actionLog(cmd, err) + + if err != nil { + os.Exit(1) + } else { + os.Exit(0) + } +} diff --git a/service/privilege.go b/service/privilege.go new file mode 100644 index 00000000..be7c824f --- /dev/null +++ b/service/privilege.go @@ -0,0 +1,36 @@ +package service + +import ( + "os" + "runtime" +) + +// isAdmin 检查当前用户是否具有管理员权限 +func isAdmin() bool { + switch runtime.GOOS { + case "windows": + return isAdminWindows() + case "linux": + return isAdminLinux() + case "darwin": + return isAdminMacOS() + default: + return false + } +} + +// isAdminWindows 检查当前用户是否具有管理员权限(Windows) +func isAdminWindows() bool { + _, err := os.Open("\\\\.\\PHYSICALDRIVE0") + return err == nil +} + +// isAdminLinux 检查当前用户是否具有管理员权限(Linux) +func isAdminLinux() bool { + return os.Geteuid() == 0 +} + +// isAdminMacOS 检查当前用户是否具有管理员权限(macOS) +func isAdminMacOS() bool { + return os.Geteuid() == 0 +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 00000000..0e93e9df --- /dev/null +++ b/service/service.go @@ -0,0 +1,76 @@ +package service + +import ( + "github.com/kardianos/service" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "os" + "path/filepath" +) + +type program struct{} + +var ( + SvcConfig *service.Config + Program *program + SysService service.Service + Elevated bool + mainFunc func() + Noninteractive bool +) + +func Init(runFunc func()) (service.Status, error) { + Elevated = isAdmin() + mainFunc = runFunc + Noninteractive = false + + err := error(nil) + svcHomeDir, err := filepath.Abs(C.Path.HomeDir()) + SvcConfig = &service.Config{ + Name: "mihomo-kernel", + DisplayName: "Mihomo Kernel", + Description: "Another Mihomo Kernel.", + Arguments: []string{ + "--service", "noninteractive", + "-d", svcHomeDir, + }, + //Dependencies: []string{ + // "Requires=network.target", + // "After=network-online.target"}, + Option: service.KeyValue{ + "Restart": "on-success", + "SuccessExitStatus": "1 2 8 SIGKILL", + }, + } + + Program = &program{} + SysService, err = service.New(Program, SvcConfig) + if err != nil { + log.Errorln("Fatal: %s", err) + os.Exit(1) + } + return SysService.Status() +} + +func SetLogger(fileDir string) { + filePath := filepath.Join(fileDir, "service.log") + + f, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777) + if err != nil { + os.Stdout = f + os.Stderr = f + } +} + +func actionLog(action string, err error) { + if err == nil { + log.Infoln("Successfully to %s a service.", action) + return + } + + if !Elevated { + log.Errorln("Service control action needs elevated privileges. Please run with administrator privileges.") + } + log.Errorln("Failed to %s a service: %s", action, err) + +}