使用kardianos-service 创建golang开机自启动服务

开机自启动服务在实际的应用中还是比较多的,kardianos-service 是golang 的一个很不错的实现,我们增强我们
golang 应用的可管理性,以下是一个实践说明

基本使用

此代码比较简单

  • 代码
package main
import (
    "flag"
    "log"
    "time"
    "github.com/kardianos/service"
)
var logger service.Logger
// Program structures.
//  Define Start and Stop methods.
type program struct {
    exit chan struct{}
}
func (p *program) Start(s service.Service) error {
    if service.Interactive() {
        logger.Info("Running in terminal.")
    } else {
        logger.Info("Running under service manager.")
    }
    p.exit = make(chan struct{})
    // Start should not block. Do the actual work async.
    go p.run()
    return nil
}
func (p *program) run() error {
    logger.Infof("I'm running %v.", service.Platform())
    ticker := time.NewTicker(2 * time.Second)
    for {
        select {
        case tm := <-ticker.C:
            logger.Infof("Still running at %v...", tm)
        case <-p.exit:
            ticker.Stop()
            return nil
        }
    }
}
func (p *program) Stop(s service.Service) error {
    // Any work in Stop should be quick, usually a few seconds at most.
    logger.Info("I'm Stopping!")
    close(p.exit)
    return nil
}
// Service setup.
//   Define service config.
//   Create the service.
//   Setup the logger.
//   Handle service controls (optional).
//   Run the service.
func main() {
    svcFlag := flag.String("service", "", "Control the system service.")
    flag.Parse()
    options := make(service.KeyValue)
    options["Restart"] = "on-success"
    options["SuccessExitStatus"] = "1 2 8 SIGKILL"
    svcConfig := &service.Config{
        Name:        "GoServiceExampleLogging",
        DisplayName: "Go Service Example for Logging",
        Description: "This is an example Go service that outputs log messages.",
        Dependencies: []string{
            "Requires=network.target",
            "After=network-online.target syslog.target"},
        Option: options,
    }
    prg := &program{}
    s, err := service.New(prg, svcConfig)
    if err != nil {
        log.Fatal(err)
    }
    errs := make(chan error, 5)
    logger, err = s.Logger(errs)
    if err != nil {
        log.Fatal(err)
    }
    go func() {
        for {
            err := <-errs
            if err != nil {
                log.Print(err)
            }
        }
    }()
    if len(*svcFlag) != 0 {
        err := service.Control(s, *svcFlag)
        if err != nil {
            log.Printf("Valid actions: %q\n", service.ControlAction)
            log.Fatal(err)
        }
        return
    }
    err = s.Run()
    if err != nil {
        logger.Error(err)
    }
}
  • 重点
    实现一个服务的golang 应用,应该实现以下接口
 
Start // start 方法不行block,可以基于goruntine 解决
Stop

一个参考集成cli 的代码

没有提供完整代码,集成了logger 以及基于urfave 的cli 服务

package commands
import (
    "fmt"
    "log"
    "os"
    "time"
    "github.com/kardianos/service"
    "github.com/robfig/cron/v3"
    "github.com/urfave/cli/v2"
    "github.com/rongfengliang/sql-server-exporter/pkg/agent"
    "github.com/rongfengliang/sql-server-exporter/pkg/buildinfo"
    "github.com/rongfengliang/sql-server-exporter/pkg/jobs"
)
var (
    logger service.Logger
    errs   chan error
)
const (
    AGENTNAME = "sql-data-exporter-agent"
    JOBNAME   = "sql-data-exporter-job"
)
// Server server
type Server struct {
    jobdirconf string
    cron       *cron.Cron
}
type ServerWrapper struct {
    *Server
    jobdirconf string
}
func NewServer() *Server {
    return &Server{}
}
func init() {
    errs = make(chan error, 5)
}
func newServerConf(jobdirconf string) *Server {
    return &Server{
        jobdirconf: jobdirconf,
    }
}
// NewServerWrapper newServerWrapper
func NewServerWrapper(jobdirconf string) *ServerWrapper {
    return &ServerWrapper{
        Server:     newServerConf(jobdirconf),
        jobdirconf: jobdirconf,
    }
}
// Start start job Server
func (server *ServerWrapper) Start(s service.Service) error {
    // Start should not block. Do the actual work async.
    if service.Interactive() {
        logger.Info("Running in terminal.")
    } else {
        logger.Info("Running under service manager.")
    }
    go server.run(server.jobdirconf)
    return nil
}
// Stop Stop
func (server *ServerWrapper) Stop(s service.Service) error {
    time.Sleep(2 * time.Second)
    err := server.stop()
    return err
}
func (server *Server) run(jobdirconf string) error {
    jobdir := jobdirconf
    logger.Info("job is running")
    if jobdir != "" {
        loadJobs, cronhub, err := jobs.ParseJobs(jobdir)
        if err != nil {
            return err
        }
        server.cron = cronhub
        for _, v := range loadJobs {
            logger.Info("js engine:" + v.EngineName)
        }
        go cronhub.Run()
    }
    return nil
}
func (server *Server) stop() error {
    ctx := server.cron.Stop()
    select {
    case <-ctx.Done():
        return ctx.Err()
    }
}
// Serve Serve job service
func (server *Server) Serve() error {
    // TODos
    // load jobs create scheduler info
    app := cli.NewApp()
    app.Usage = "basic sql server data fetch service"
    app.Flags = []cli.Flag{
        &cli.StringFlag{
            Name:  "jobdirconf",
            Usage: "set job dirs",
            Value: ".",
        },
        &cli.StringFlag{
            Name:  "dbconf",
            Usage: "db agent config",
            Value: "",
        },
    }
    app.Commands = []*cli.Command{
        {
            Name:    "version",
            Aliases: []string{"v"},
            Usage:   "print application version",
            Action: func(c *cli.Context) error {
                fmt.Println(buildinfo.Version)
                return nil
            },
        },
        {
            Name:  "job",
            Usage: "start job service",
            Action: func(c *cli.Context) error {
                jobdir := c.String("jobdirconf")
                if jobdir != "" {
                    serviceConfig := &service.Config{
                        Name:        "sql-data-exporter-job",
                        Description: "sql-data-exporter-job",
                        DisplayName: "sql-data-exporter-job",
                    }
                    server := NewServerWrapper(jobdir)
                    s, err := service.New(server, serviceConfig)
                    if err != nil {
                        return err
                    }
                    logger, err = s.SystemLogger(errs)
                    if err != nil {
                        return err
                    }
                    err = s.Run()
                    if err != nil {
                        return err
                    }
                    return nil
                }
                return nil
            },
        },
        {
            Name:  "agent",
            Usage: "start agent service",
            Action: func(c *cli.Context) error {
                dbconfig := c.String("dbconf")
                if dbconfig != "" {
                    serviceConfig := &service.Config{
                        Name:        "sql-data-exporter-agent",
                        Description: "sql-data-exporter-agent",
                        DisplayName: "sql-data-exporter-agent",
                    }
                    server := agent.NewAgentWrapper(dbconfig)
                    s, err := service.New(server, serviceConfig)
                    if err != nil {
                        return err
                    }
                    logger, err = s.SystemLogger(errs)
                    if err != nil {
                        log.Fatal(err.Error())
                    }
                    server.Logger = logger
                    s.Run()
                    return nil
                }
                return nil
            },
        },
        {
            Name:  "service",
            Usage: "system service operator",
            Subcommands: []*cli.Command{
                {
                    Name:  "agent",
                    Usage: "agent servie operator",
                    Subcommands: []*cli.Command{
                        {
                            Name:  "install",
                            Usage: "agent servie install",
                            Action: func(c *cli.Context) error {
                                dbconfig := c.String("dbconf")
                                if dbconfig == "" {
                                    dbconfig = "agent.hcl"
                                }
                                agentname := c.String("agentname")
                                if agentname == "" {
                                    agentname = AGENTNAME
                                }
                                serviceConfig := &service.Config{
                                    Name:        agentname,
                                    DisplayName: AGENTNAME,
                                    Description: AGENTNAME,
                                    Arguments:   []string{"--dbconf", dbconfig, "agent"},
                                }
                                server := agent.NewAgentWrapper(dbconfig)
                                s, err := service.New(server, serviceConfig)
                                if err != nil {
                                    fmt.Println("install service wrong: " + err.Error())
                                    return err
                                }
                                s.Install()
                                return nil
                            },
                        },
                    },
                },
                {
                    Name:  "job",
                    Usage: "job servie operator",
                    Subcommands: []*cli.Command{
                        {
                            Name:  "install",
                            Usage: "job servie install",
                            Action: func(c *cli.Context) error {
                                jobdir := c.String("jobdirconf")
                                if jobdir == "" {
                                    jobdir = "jobs"
                                }
                                jobtname := c.String("jobname")
                                if jobtname == "" {
                                    jobtname = JOBNAME
                                }
                                serviceConfig := &service.Config{
                                    Name:        jobtname,
                                    DisplayName: JOBNAME,
                                    Description: JOBNAME,
                                    Arguments:   []string{"--jobdirconf", jobdir, "job"},
                                }
                                server := NewServerWrapper(jobdir)
                                s, err := service.New(server, serviceConfig)
                                if err != nil {
                                    fmt.Println("install service wrong: " + err.Error())
                                    return err
                                }
                                s.Install()
                                return nil
                            },
                        },
                    },
                },
            },
        },
    }
    err := app.Run(os.Args)
    if err != nil {
        return err
    }
    return nil
}
  • 参考效果
./bin/exporter-server service
NAME:
   exporter-server service - system service operator
USAGE:
   exporter-server service command [command options] [arguments...]
COMMANDS:
   agent    agent servie operator
   job      job servie operator
   help, h  Shows a list of commands or help for one command
OPTIONS:
   --help, -h  show help (default: false)

说明

我们如果依赖了一些配置(比如关于路径的,需要使用绝对路径),Start 部分需要使用异步模式,kardianos-service
暴露的Run 方法很重要,我们需要运行(服务管理模式的需要),对于我们原有的代码, 可以通过golang的组合以及包装
接口提供类似的服务,这样可以系统对于kardianos-service 的依赖可以减少,提高代码的复用

参考资料

https://github.com/kardianos/service