使用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 的依赖可以减少,提高代码的复用