1. 程式人生 > 實用技巧 >使用kardianos-service 建立golang開機自啟動服務

使用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