学习篇-进阶

注册 windows 服务

文章源码地址:https://www.github.com/snowlyg/learns

将前面的程序注册成 windows 服务有多种方式:

其中 https://github.com/kardianos 比较适合实现自动更新项目的需求,这个项目是兼容 linux 、mac windows 等多个系统环境的。但是我们只需要 windows 系统环境,如果是其他环境下更新一个程序其实并不需要这么麻烦。

查看源码能看到 windwos 系统使用的是这个包 golang.org/x/sys/windows , 用这个包简单的写一个新的 package。

  • 项目目录下新建 windows 文件夹,并新建7个文件:
  • service.go
  • beep.go
  • install.go
  • start.go
  • stop.go
  • status.go
  • uninstall.go

service.go 文件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package windows

import (
	"fmt"
	"os"
	"os/signal"
	"time"

	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/debug"
	"golang.org/x/sys/windows/svc/eventlog"
)

var (
	elog        debug.Log
	interactive = false
)

// init 初始化
// IsAnInteractiveSession 判断程序是否运行在交互模式
// 此方法说是要被废弃了,后期要使用 isWindowsService 代替
// 不过我使用次方法无法正常启动注册好的程序
func init() {
	var err error
	interactive, err = svc.IsAnInteractiveSession()
	if err != nil {
		panic(err)
	}
}

// Interface
type Interface interface {
	Start() error
	Stop() error
}

// WindowsService
type WindowsService struct {
	Name string 
	i    Interface
}

// NewWindowsService new一个服务对象
// name 服务名称
func NewWindowsService(i Interface, name string) (*WindowsService, error) {
	ws := &WindowsService{
		i:    i,
		Name: name,
	}
	return ws, nil
}

// Execute 服务执行方法,控制服务启动停止
func (ws *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
	changes <- svc.Status{State: svc.StartPending}

	if err := ws.i.Start(); err != nil {
		elog.Info(1, fmt.Sprintf("%s service start failed: %v", ws.Name, err))
		return true, 1
	}

	changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
	for {
		c := <-r
		switch c.Cmd {
		case svc.Interrogate:
			changes <- c.CurrentStatus
		case svc.Stop:
			changes <- svc.Status{State: svc.StopPending}
			if err := ws.i.Stop(); err != nil {
				elog.Info(1, fmt.Sprintf("%s service stop failed: %v", ws.Name, err))
				return true, 2
			}
			break loop
		case svc.Shutdown:
			changes <- svc.Status{State: svc.StopPending}
			err := ws.i.Stop()
			if err != nil {
				elog.Info(1, fmt.Sprintf("%s service shutdown failed: %v", ws.Name, err))
				return true, 2
			}
			break loop
		default:
			continue loop
		}
	}

	return false, 0
}

func (ws *WindowsService) Run(isDebug bool) error {
	var err error
	if !interactive {
		if isDebug {
			elog = debug.New(ws.Name)
		} else {
			elog, err = eventlog.Open(ws.Name)
			if err != nil {
				return err
			}
		}
		defer elog.Close()

		elog.Info(1, fmt.Sprintf("starting %s service", ws.Name))
		run := svc.Run
		if isDebug {
			run = debug.Run
		}
		err = run(ws.Name, ws)
		if err != nil {
			elog.Error(1, fmt.Sprintf("%s service failed: %v", ws.Name, err))
			return err
		}
		elog.Info(1, fmt.Sprintf("%s service stopped", ws.Name))
	}

	err = ws.i.Start()
	if err != nil {
		return err
	}

	sigChan := make(chan os.Signal)

	signal.Notify(sigChan, os.Interrupt)

	<-sigChan

	return ws.i.Stop()
}

beep.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package windows

import (
	"syscall"
)

// BUG(brainman): MessageBeep Windows api is broken on Windows 7,
// so this example does not beep when runs as service on Windows 7.

var (
	beepFunc = syscall.MustLoadDLL("user32.dll").MustFindProc("MessageBeep")
)

func beep() {
	beepFunc.Call(0xffffffff)
}

install.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package windows

import (
	"fmt"

	"golang.org/x/sys/windows/svc/eventlog"
	"golang.org/x/sys/windows/svc/mgr"
)

func ServiceInstall(svcName, execPath, dispalyName string, args ...string) error {
	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()
	s, err := m.OpenService(svcName)
	if err == nil {
		s.Close()
		return fmt.Errorf("service %s already exists", svcName)
	}
	config := mgr.Config{
		DisplayName: dispalyName,
		StartType:   mgr.StartAutomatic,
	}
	if len(args) >= 2 {
		config.ServiceStartName = args[0]
		config.Password = args[1]
	}
	s, err = m.CreateService(svcName, execPath, config)
	if err != nil {
		return fmt.Errorf("CreateService() failed: %s", err)
	}
	defer s.Close()
	err = eventlog.InstallAsEventCreate(svcName, eventlog.Error|eventlog.Warning|eventlog.Info)
	if err != nil {
		s.Delete()
		return fmt.Errorf("InstallAsEventCreate() failed: %s", err)
	}
	return nil
}

start.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package windows

import (
	"golang.org/x/sys/windows/svc/mgr"
)

func ServiceStart(srcName string) error {
	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()

	s, err := m.OpenService(srcName)
	if err != nil {
		return err
	}
	defer s.Close()
	return s.Start()
}

status.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package windows

import (
	"errors"
	"fmt"

	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

type Status byte

const (
	StatusUnknown Status = iota // Status is unable to be determined due to an error or it was not installed.
	StatusRunning
	StatusStopped
	StatusUninstall
)

var (
	// ErrNameFieldRequired is returned when Config.Name is empty.
	ErrNameFieldRequired = errors.New("Config.Name field is required.")
	// ErrNoServiceSystemDetected is returned when no system was detected.
	ErrNoServiceSystemDetected = errors.New("No service system detected.")
	// ErrNotInstalled is returned when the service is not installed
	ErrNotInstalled = errors.New("the service is not installed")
)

// status
func ServiceStatus(srcName string) (Status, error) {
	m, err := mgr.Connect()
	if err != nil {
		return StatusUnknown, err
	}
	defer m.Disconnect()

	s, err := m.OpenService(srcName)
	if err != nil {
		if err.Error() == "The specified service does not exist as an installed service." {
			return StatusUninstall, nil
		}
		return StatusUnknown, err
	}
	defer s.Close()

	status, err := s.Query()
	if err != nil {
		return StatusUnknown, err
	}

	switch status.State {
	case svc.StartPending:
		fallthrough
	case svc.Running:
		return StatusRunning, nil
	case svc.PausePending:
		fallthrough
	case svc.Paused:
		fallthrough
	case svc.ContinuePending:
		fallthrough
	case svc.StopPending:
		fallthrough
	case svc.Stopped:
		return StatusStopped, nil
	default:
		return StatusUnknown, fmt.Errorf("unknown status %v", status)
	}
}

stop.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package windows

import (
	"strconv"
	"time"

	"golang.org/x/sys/windows/registry"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

func ServiceStop(srcName string) error {
	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()

	s, err := m.OpenService(srcName)
	if err != nil {
		return err
	}
	defer s.Close()

	return stopWait(s)
}

func stopWait(s *mgr.Service) error {
	// First stop the service. Then wait for the service to
	// actually stop before starting it.
	status, err := s.Control(svc.Stop)
	if err != nil {
		return err
	}

	timeDuration := time.Millisecond * 50

	timeout := time.After(getStopTimeout() + (timeDuration * 2))
	tick := time.NewTicker(timeDuration)
	defer tick.Stop()

	for status.State != svc.Stopped {
		select {
		case <-tick.C:
			status, err = s.Query()
			if err != nil {
				return err
			}
		case <-timeout:
			break
		}
	}
	return nil
}

// getStopTimeout fetches the time before windows will kill the service.
func getStopTimeout() time.Duration {
	// For default and paths see https://support.microsoft.com/en-us/kb/146092
	defaultTimeout := time.Millisecond * 20000
	key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control`, registry.READ)
	if err != nil {
		return defaultTimeout
	}
	sv, _, err := key.GetStringValue("WaitToKillServiceTimeout")
	if err != nil {
		return defaultTimeout
	}
	v, err := strconv.Atoi(sv)
	if err != nil {
		return defaultTimeout
	}
	return time.Millisecond * time.Duration(v)
}

uninstall.go 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package windows

import (
	"fmt"

	"golang.org/x/sys/windows/svc/eventlog"
	"golang.org/x/sys/windows/svc/mgr"
)

func ServiceUninstall(srcName string) error {
	m, err := mgr.Connect()
	if err != nil {
		return err
	}
	defer m.Disconnect()
	s, err := m.OpenService(srcName)
	if err != nil {
		return fmt.Errorf("service %s is not installed", srcName)
	}
	defer s.Close()
	err = s.Delete()
	if err != nil {
		return err
	}
	err = eventlog.Remove(srcName)
	if err != nil {
		return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
	}
	return nil
}

修改前面的 main.go 文件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/snowlyg/learns/advance/windows"
)

func run() {
	// 每隔 5 秒打印当前时间
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	// for 循环阻塞程序主进程
	// ticker.C 通道每隔五秒会发送一个值
	for {
		<-ticker.C
		// 格式化时间
		now := time.Now().Format(time.RFC3339)
		fmt.Printf("当前时间:%s \n", now)
	}
}

func (p *program) Start() error {
	go run()
	return nil
}

func (p *program) Stop() error {
	//stop
	return nil
}

type program struct{}

// usage 获取终端输入参数
func usage(errmsg string) {
	fmt.Fprintf(os.Stderr,
		"%s\n\n"+
			"usage: %s <command> <servicename>\n"+
			"       where <command> is one of\n"+
			"       install, remove, start, stop, status .\n"+
			"       and use install : .\n"+
			"       install <service name> <exec path> <display name> <system name> <password>  \n",
		errmsg, os.Args[0])
	os.Exit(2)
}

func main() {
	// new 一个服务
	s, err := windows.NewWindowsService(&program{}, "myservice")
	if err != nil {
		fmt.Printf("new service get error %v \n", err)
	}
	if len(os.Args) >= 2 {

		srvName := strings.ToLower(os.Args[2])
		cmd := strings.ToLower(os.Args[1])
		switch cmd {
		case "start": // 启动
			err := windows.ServiceStart(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("start success")
			return
		case "install": //安装
			if len(os.Args) == 7 {
				err := windows.ServiceInstall(srvName, os.Args[3], os.Args[4], os.Args[5], os.Args[6])
				if err != nil {
					fmt.Printf("%v \n", err)
				}
			} else if len(os.Args) == 5 {
				err := windows.ServiceInstall(srvName, os.Args[3], os.Args[4])
				if err != nil {
					fmt.Printf("%v \n", err)
				}
			} else {
				usage("error command specified")
			}

			println("install success")
			return
		case "stop": // 停止
			err := windows.ServiceStop(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("stop success")
			return
		case "remove": // 卸载
			err := windows.ServiceUninstall(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("remove success")
			return
		case "status": // 查询服务状态
			status, _ := windows.ServiceStatus(srvName)
			if status == windows.StatusRunning {
				println("运行中")
			} else if status == windows.StatusStopped {
				println("已停止")
			} else if status == windows.StatusUninstall {
				println("未安装")
			} else if status == windows.StatusUninstall {
				println("未安装")
			}
			return
		default:
			println("invaild command")
		}
		switch cmd {
		case "start": // 启动
			err := windows.ServiceStart(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("start success")
			return
		case "install": //安装
			if len(os.Args) == 7 {
				err := windows.ServiceInstall(srvName, os.Args[3], os.Args[4], os.Args[5], os.Args[6])
				if err != nil {
					fmt.Printf("%v \n", err)
				}
			} else if len(os.Args) == 5 {
				err := windows.ServiceInstall(srvName, os.Args[3], os.Args[4])
				if err != nil {
					fmt.Printf("%v \n", err)
				}
			} else {
				usage("error command specified")
			}

			println("install success")
			return
		case "stop": // 停止
			err := windows.ServiceStop(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("stop success")
			return
		case "remove": // 卸载
			err := windows.ServiceUninstall(srvName)
			if err != nil {
				fmt.Printf("%v \n", err)
			}
			println("remove success")
			return
		case "status": // 查询服务状态
			status, _ := windows.ServiceStatus(srvName)
			if status == windows.StatusRunning {
				println("运行中")
			} else if status == windows.StatusStopped {
				println("已停止")
			} else if status == windows.StatusUninstall {
				println("未安装")
			} else if status == windows.StatusUnknown {
				println("未知状态")
			}
			return
		default:
			println("invaild command")
		}
	}
	s.Run(false)
}

打包文件

再次打包程序 go build -o myservice.exe main.go 生成 myservice.exe 文件。 可以通过如下命令行控制服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

# 注册服务,有七个参数 服务名称、服务文件路径、服务显示名称、系统账号、系统密码
# 只有 服务名称、服务文件路径 为必选参数,其他为可选参数
# 有账号密码
.\cmd\advance.exe install myservice "C:\Users\Administrator\go\src\github.com\snowlyg\learns\cmd\advance.exe" myservice  ".\Administrator" "123456"
# 没账号密码
.\cmd\advance.exe install myservice "C:\Users\Administrator\go\src\github.com\snowlyg\learns\cmd\advance.exe" myservice  

# 启动服务
.\cmd\advance.exe start myservice 

# 停止服务
.\cmd\advance.exe stop myservice 

# 卸载服务
.\cmd\advance.exe remove myservice 

# 查询服务状态
.\cmd\advance.exe status myservice 

小坑

这里有个小坑:当使用账号(administrator)、密码(123456)参数注册服务的时候,启动的时候会提示账号密码错误。这个问题我各种看源码、各种搜索都没有解决问题。最后无意中看到了这个文档https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-createservicea

这个文档我有找了几个小时才找到,毕竟已经过了好几天了。仔细看这个文档会发现这样描述: docs.png

就是最后一行,注册服务获取系统授权需要使用 .\Administrator

最后吐槽一下:windows 的文档是真的难找。