diff --git a/cmd/edge-admin/main.go b/cmd/edge-admin/main.go index 8abe15d3..89e4a4a9 100644 --- a/cmd/edge-admin/main.go +++ b/cmd/edge-admin/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "github.com/TeaOSLab/EdgeAdmin/internal/apps" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" "github.com/TeaOSLab/EdgeAdmin/internal/nodes" @@ -12,8 +13,18 @@ func main() { app := apps.NewAppCmd(). Version(teaconst.Version). Product(teaconst.ProductName). - Usage(teaconst.ProcessName + " [-v|start|stop|restart]") + Usage(teaconst.ProcessName + " [-v|start|stop|restart|service|daemon]") + app.On("daemon", func() { + nodes.NewAdminNode().Daemon() + }) + app.On("service", func() { + err := nodes.NewAdminNode().InstallSystemService() + if err != nil { + fmt.Println("[ERROR]install failed: " + err.Error()) + } + fmt.Println("done") + }) app.Run(func() { adminNode := nodes.NewAdminNode() adminNode.Run() diff --git a/internal/const/const.go b/internal/const/const.go index 2d84c17e..d5f8fdc8 100644 --- a/internal/const/const.go +++ b/internal/const/const.go @@ -14,4 +14,6 @@ const ( ErrServer = "服务器出了点小问题,请联系技术人员处理。" CookieSID = "edgesid" + + SystemdServiceName = "edge-admin" ) diff --git a/internal/nodes/admin_node.go b/internal/nodes/admin_node.go index d3ed0333..58e7c0f2 100644 --- a/internal/nodes/admin_node.go +++ b/internal/nodes/admin_node.go @@ -5,12 +5,16 @@ import ( teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" "github.com/TeaOSLab/EdgeAdmin/internal/errors" "github.com/TeaOSLab/EdgeAdmin/internal/events" + "github.com/TeaOSLab/EdgeAdmin/internal/utils" "github.com/iwind/TeaGo" "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/lists" "github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/rands" "github.com/iwind/TeaGo/sessions" "io/ioutil" + "log" + "net" "os" "os/exec" "os/signal" @@ -31,8 +35,15 @@ func (this *AdminNode) Run() { secret := this.genSecret() configs.Secret = secret + // 本地Sock + err := this.listenSock() + if err != nil { + logs.Println("NODE" + err.Error()) + return + } + // 检查server配置 - err := this.checkServer() + err = this.checkServer() if err != nil { if err != nil { logs.Println("[NODE]" + err.Error()) @@ -72,6 +83,68 @@ func (this *AdminNode) Run() { Start() } +// 实现守护进程 +func (this *AdminNode) Daemon() { + path := os.TempDir() + "/edge-admin.sock" + isDebug := lists.ContainsString(os.Args, "debug") + isDebug = true + for { + conn, err := net.DialTimeout("unix", path, 1*time.Second) + if err != nil { + if isDebug { + log.Println("[DAEMON]starting ...") + } + + // 尝试启动 + err = func() error { + exe, err := os.Executable() + if err != nil { + return err + } + cmd := exec.Command(exe) + err = cmd.Start() + if err != nil { + return err + } + err = cmd.Wait() + if err != nil { + return err + } + return nil + }() + + if err != nil { + if isDebug { + log.Println("[DAEMON]", err) + } + time.Sleep(1 * time.Second) + } else { + time.Sleep(5 * time.Second) + } + } else { + _ = conn.Close() + time.Sleep(5 * time.Second) + } + } +} + +// 安装系统服务 +func (this *AdminNode) InstallSystemService() error { + shortName := teaconst.SystemdServiceName + + exe, err := os.Executable() + if err != nil { + return err + } + + manager := utils.NewServiceManager(shortName, teaconst.ProductName) + err = manager.Install(exe, []string{}) + if err != nil { + return err + } + return nil +} + // 检查Server配置 func (this *AdminNode) checkServer() error { configFile := Tea.ConfigFile("server.yaml") @@ -143,3 +216,40 @@ func (this *AdminNode) genSecret() string { _ = ioutil.WriteFile(tmpFile, []byte(secret), 0666) return secret } + +// 监听本地sock +func (this *AdminNode) listenSock() error { + path := os.TempDir() + "/edge-admin.sock" + + // 检查是否已经存在 + _, err := os.Stat(path) + if err == nil { + conn, err := net.Dial("unix", path) + if err != nil { + _ = os.Remove(path) + } else { + _ = conn.Close() + } + } + + // 新的监听任务 + listener, err := net.Listen("unix", path) + if err != nil { + return err + } + events.On(events.EventQuit, func() { + logs.Println("NODE", "quit unix sock") + _ = listener.Close() + }) + + go func() { + for { + _, err := listener.Accept() + if err != nil { + return + } + } + }() + + return nil +} diff --git a/internal/utils/service.go b/internal/utils/service.go new file mode 100644 index 00000000..8da91468 --- /dev/null +++ b/internal/utils/service.go @@ -0,0 +1,111 @@ +package utils + +import ( + "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/files" + "github.com/iwind/TeaGo/logs" + "log" + "os" + "path/filepath" + "runtime" + "sync" +) + +// 服务管理器 +type ServiceManager struct { + Name string + Description string + + fp *os.File + logger *log.Logger + onceLocker sync.Once +} + +// 获取对象 +func NewServiceManager(name, description string) *ServiceManager { + manager := &ServiceManager{ + Name: name, + Description: description, + } + + // root + manager.resetRoot() + + return manager +} + +// 设置服务 +func (this *ServiceManager) setup() { + this.onceLocker.Do(func() { + logFile := files.NewFile(Tea.Root + "/logs/service.log") + if logFile.Exists() { + logFile.Delete() + } + + //logger + fp, err := os.OpenFile(Tea.Root+"/logs/service.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + logs.Error(err) + return + } + this.fp = fp + this.logger = log.New(fp, "", log.LstdFlags) + }) +} + +// 记录普通日志 +func (this *ServiceManager) Log(msg string) { + this.setup() + if this.logger == nil { + return + } + this.logger.Println("[info]" + msg) +} + +// 记录错误日志 +func (this *ServiceManager) LogError(msg string) { + this.setup() + if this.logger == nil { + return + } + this.logger.Println("[error]" + msg) +} + +// 关闭 +func (this *ServiceManager) Close() error { + if this.fp != nil { + return this.fp.Close() + } + return nil +} + +// 重置Root +func (this *ServiceManager) resetRoot() { + if !Tea.IsTesting() { + exePath, err := os.Executable() + if err != nil { + exePath = os.Args[0] + } + link, err := filepath.EvalSymlinks(exePath) + if err == nil { + exePath = link + } + fullPath, err := filepath.Abs(exePath) + if err == nil { + Tea.UpdateRoot(filepath.Dir(filepath.Dir(fullPath))) + } + } + Tea.SetPublicDir(Tea.Root + Tea.DS + "web" + Tea.DS + "public") + Tea.SetViewsDir(Tea.Root + Tea.DS + "web" + Tea.DS + "views") + Tea.SetTmpDir(Tea.Root + Tea.DS + "web" + Tea.DS + "tmp") +} + +// 保持命令行窗口是打开的 +func (this *ServiceManager) PauseWindow() { + if runtime.GOOS != "windows" { + return + } + + b := make([]byte, 1) + _, _ = os.Stdin.Read(b) +} diff --git a/internal/utils/service_linux.go b/internal/utils/service_linux.go new file mode 100644 index 00000000..82533418 --- /dev/null +++ b/internal/utils/service_linux.go @@ -0,0 +1,154 @@ +// +build linux + +package utils + +import ( + "errors" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/files" + "io/ioutil" + "os" + "os/exec" + "regexp" +) + +var systemdServiceFile = "/etc/systemd/system/edge-admin.service" +var initServiceFile = "/etc/init.d/" + teaconst.SystemdServiceName + +// 安装服务 +func (this *ServiceManager) Install(exePath string, args []string) error { + if os.Getgid() != 0 { + return errors.New("only root users can install the service") + } + + systemd, err := exec.LookPath("systemctl") + if err != nil { + return this.installInitService(exePath, args) + } + + return this.installSystemdService(systemd, exePath, args) +} + +// 启动服务 +func (this *ServiceManager) Start() error { + if os.Getgid() != 0 { + return errors.New("only root users can start the service") + } + + if files.NewFile(systemdServiceFile).Exists() { + systemd, err := exec.LookPath("systemctl") + if err != nil { + return err + } + + return exec.Command(systemd, "start", teaconst.SystemdServiceName+".service").Start() + } + return exec.Command("service", teaconst.ProcessName, "start").Start() +} + +// 删除服务 +func (this *ServiceManager) Uninstall() error { + if os.Getgid() != 0 { + return errors.New("only root users can uninstall the service") + } + + if files.NewFile(systemdServiceFile).Exists() { + systemd, err := exec.LookPath("systemctl") + if err != nil { + return err + } + + // disable service + exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Start() + + // reload + exec.Command(systemd, "daemon-reload") + + return files.NewFile(systemdServiceFile).Delete() + } + + f := files.NewFile(initServiceFile) + if f.Exists() { + return f.Delete() + } + return nil +} + +// install init service +func (this *ServiceManager) installInitService(exePath string, args []string) error { + shortName := teaconst.SystemdServiceName + scriptFile := Tea.Root + "/scripts/" + shortName + if !files.NewFile(scriptFile).Exists() { + return errors.New("'scripts/" + shortName + "' file not exists") + } + + data, err := ioutil.ReadFile(scriptFile) + if err != nil { + return err + } + + data = regexp.MustCompile("INSTALL_DIR=.+").ReplaceAll(data, []byte("INSTALL_DIR="+Tea.Root)) + err = ioutil.WriteFile(initServiceFile, data, 0777) + if err != nil { + return err + } + + chkCmd, err := exec.LookPath("chkconfig") + if err != nil { + return err + } + + err = exec.Command(chkCmd, "--add", teaconst.ProcessName).Start() + if err != nil { + return err + } + + return nil +} + +// install systemd service +func (this *ServiceManager) installSystemdService(systemd, exePath string, args []string) error { + shortName := teaconst.SystemdServiceName + longName := "GoEdge API" // TODO 将来可以修改 + + desc := `# Provides: ` + shortName + ` +# Required-Start: $all +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: +# Short-Description: ` + longName + ` Service +### END INIT INFO + +[Unit] +Description=` + longName + ` Service +Before=shutdown.target +After=network-online.target + +[Service] +Type=simple +Restart=always +RestartSec=1s +ExecStart=` + exePath + ` daemon +ExecStop=` + exePath + ` stop +ExecReload=` + exePath + ` reload + +[Install] +WantedBy=multi-user.target` + + // write file + err := ioutil.WriteFile(systemdServiceFile, []byte(desc), 0777) + if err != nil { + return err + } + + // stop current systemd service if running + exec.Command(systemd, "stop", shortName+".service") + + // reload + exec.Command(systemd, "daemon-reload") + + // enable + cmd := exec.Command(systemd, "enable", shortName+".service") + return cmd.Run() +} diff --git a/internal/utils/service_others.go b/internal/utils/service_others.go new file mode 100644 index 00000000..43755dee --- /dev/null +++ b/internal/utils/service_others.go @@ -0,0 +1,18 @@ +// +build !linux,!windows + +package utils + +// 安装服务 +func (this *ServiceManager) Install(exePath string, args []string) error { + return nil +} + +// 启动服务 +func (this *ServiceManager) Start() error { + return nil +} + +// 删除服务 +func (this *ServiceManager) Uninstall() error { + return nil +} diff --git a/internal/utils/service_test.go b/internal/utils/service_test.go new file mode 100644 index 00000000..163a579f --- /dev/null +++ b/internal/utils/service_test.go @@ -0,0 +1,12 @@ +package utils + +import ( + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "testing" +) + +func TestServiceManager_Log(t *testing.T) { + manager := NewServiceManager(teaconst.ProductName, teaconst.ProductName+" Server") + manager.Log("Hello, World") + manager.LogError("Hello, World") +} diff --git a/internal/utils/service_windows.go b/internal/utils/service_windows.go new file mode 100644 index 00000000..8faabb4f --- /dev/null +++ b/internal/utils/service_windows.go @@ -0,0 +1,173 @@ +// +build windows + +package utils + +import ( + "fmt" + "github.com/iwind/TeaGo/Tea" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" + "os/exec" +) + +// 安装服务 +func (this *ServiceManager) Install(exePath string, args []string) error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("connecting: %s please 'Run as administrator' again", err.Error()) + } + defer m.Disconnect() + s, err := m.OpenService(this.Name) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", this.Name) + } + + s, err = m.CreateService(this.Name, exePath, mgr.Config{ + DisplayName: this.Name, + Description: this.Description, + StartType: windows.SERVICE_AUTO_START, + }, args...) + if err != nil { + return fmt.Errorf("creating: %s", err.Error()) + } + defer s.Close() + + return nil +} + +// 启动服务 +func (this *ServiceManager) Start() error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(this.Name) + if err != nil { + return fmt.Errorf("could not access service: %v", err) + } + defer s.Close() + err = s.Start("service") + if err != nil { + return fmt.Errorf("could not start service: %v", err) + } + + return nil +} + +// 删除服务 +func (this *ServiceManager) Uninstall() error { + m, err := mgr.Connect() + if err != nil { + return fmt.Errorf("connecting: %s please 'Run as administrator' again", err.Error()) + } + defer m.Disconnect() + s, err := m.OpenService(this.Name) + if err != nil { + return fmt.Errorf("open service: %s", err.Error()) + } + + // shutdown service + _, err = s.Control(svc.Stop) + if err != nil { + fmt.Printf("shutdown service: %s\n", err.Error()) + } + + defer s.Close() + err = s.Delete() + if err != nil { + return fmt.Errorf("deleting: %s", err.Error()) + } + return nil +} + +// 运行 +func (this *ServiceManager) Run() { + err := svc.Run(this.Name, this) + if err != nil { + this.LogError(err.Error()) + } +} + +// 同服务管理器的交互 +func (this *ServiceManager) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + + changes <- svc.Status{ + State: svc.StartPending, + } + + changes <- svc.Status{ + State: svc.Running, + Accepts: cmdsAccepted, + } + + // start service + this.Log("start") + this.cmdStart() + +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + this.Log("cmd: Interrogate") + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + this.Log("cmd: Stop|Shutdown") + + // stop service + this.cmdStop() + + break loop + case svc.Pause: + this.Log("cmd: Pause") + + // stop service + this.cmdStop() + + changes <- svc.Status{ + State: svc.Paused, + Accepts: cmdsAccepted, + } + case svc.Continue: + this.Log("cmd: Continue") + + // start service + this.cmdStart() + + changes <- svc.Status{ + State: svc.Running, + Accepts: cmdsAccepted, + } + default: + this.LogError(fmt.Sprintf("unexpected control request #%d\r\n", c)) + } + } + } + changes <- svc.Status{ + State: svc.StopPending, + } + return +} + +// 启动Web服务 +func (this *ServiceManager) cmdStart() { + cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "start") + err := cmd.Start() + if err != nil { + this.LogError(err.Error()) + } +} + +// 停止Web服务 +func (this *ServiceManager) cmdStop() { + cmd := exec.Command(Tea.Root+Tea.DS+"bin"+Tea.DS+teaconst.SystemdServiceName+".exe", "stop") + err := cmd.Start() + if err != nil { + this.LogError(err.Error()) + } +}