Basic support system service mode

todo: Log redirection and parameter forwarding
This commit is contained in:
xxnuo 2024-01-12 01:24:36 +08:00
parent d2d8c0115c
commit 9bb52164ed
7 changed files with 235 additions and 5 deletions

1
go.mod
View file

@ -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

3
go.sum
View file

@ -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=

25
main.go
View file

@ -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

30
service/control.go Normal file
View file

@ -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)
}

69
service/parser.go Normal file
View file

@ -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)
}
}

36
service/privilege.go Normal file
View file

@ -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
}

76
service/service.go Normal file
View file

@ -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)
}