作为一个深度mac用户,突然项目需要做一个windows服务,就很痛苦。用过golang的都知道,回不到 .net了,那就想办法用golang实现吧。
程序结构
- windows服务部分(service目录)
- 执行部分(app目录)
首先编写服务部分
service/main.go
入口程序,主要用于注册、卸载服务。
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
"github.com/kardianos/service"
)
var serviceConfig = &service.Config{
Name: "EtaxHelper",
DisplayName: "Etax大树企服税务自动化工具",
Description: "这里填写对项目的描述"
var logger service.Logger // 系统日志,在windows日志管理器查看日志
func main() {
// 构建服务对象
prog := &Program{}
s, err := service.New(prog, serviceConfig)
if err != nil {
log.Fatal(err)
}
// 用于记录系统日志
var errlog error
logger, errlog = s.Logger(nil)
if errlog != nil {
log.Fatal(err)
}
if len(os.Args) < 2 {
err = s.Run()
if err != nil {
logger.Error(err)
}
return
}
cmd := os.Args[1]
if cmd == "install" {
err = s.Install()
if err != nil {
log.Fatal(err)
}
fmt.Println("安装成功")
s.Start()
}
if cmd == "uninstall" {
s.Stop()
err = s.Uninstall()
if err != nil {
log.Fatal(err)
}
if err != nil {
log.Fatal(err)
}
fmt.Println("卸载成功")
}
// install, uninstall, start, stop 的另一种实现方式
// err = service.Control(s, os.Args[1])
// if err != nil {
// log.Fatal(err)
// }
}
type Program struct{}
func (p *Program) Start(s service.Service) error {
log.Println("开始服务")
go p.run()
return nil
}
func (p *Program) Stop(s service.Service) error {
log.Println("停止服务")
cupath, _ := getCurrentPath()
// log.Println("path:", fmt.Sprintf("%sEtaxHelper.exe", p), fmt.Sprintf("%sEtaxHelper.exe run", p), p)
lockFile := fmt.Sprintf("%slock.pid", cupath)
lock, err := os.Open(lockFile)
defer lock.Close()
if err == nil {
filePid, err := ioutil.ReadAll(lock)
if err == nil {
pidStr := fmt.Sprintf("%s", filePid)
pid, _ := strconv.Atoi(pidStr)
x, err := os.FindProcess(pid)
if err == nil {
fmt.Printf("[ERROR] 工具已启动[%s].", pidStr)
if err := x.Kill(); err != nil {
logger.Error("err kill", err)
} else {
logger.Info("killed pid", pid)
}
} else {
logger.Warning("err FindProcess", err)
}
} else {
logger.Warning("not read pid file", err)
}
} else {
logger.Warning("not open pid file", err)
}
return nil
}
func (p *Program) run() {
// service/run.go
runit()
}
service/run.go
运行程序,主要用于调用同目录下的EtaxHelper.exe这个程序。
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func getCurrentPath() (string, error) {
file, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
path, err := filepath.Abs(file)
if err != nil {
return "", err
}
i := strings.LastIndex(path, "/")
if i < 0 {
i = strings.LastIndex(path, "\\")
}
if i < 0 {
return "", errors.New(`error: Can't find "/" or "\".`)
}
return string(path[0 : i+1]), nil
}
func runit() {
p, _ := getCurrentPath()
// 这里循环主要是避免用户还没登录的时候,无法运行。每5秒尝试一次启动app,StartProcessAsCurrentUser第三个参数确定是否以管理员身份运行。
go func() {
for {
if err := StartProcessAsCurrentUser(fmt.Sprintf("%sEtaxHelper.exe", p), fmt.Sprintf("%sEtaxHelper.exe run", p), p, true); err == nil {
break
}
time.Sleep(5 * time.Second)
}
}()
}
service/service.go
服务运行模块,这部分很重要,用于以当前用户来运行exe,这部分可以在任务管理器里看到exe的运行用户。(service以SYSTEM权限运行,app以登录用户权限运行)。这部分代码来自Github https://gist.github.com/LiamHaworth/1ac37f7fb6018293fc43f86993db24fc。
package main
import (
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
var (
modwtsapi32 *windows.LazyDLL = windows.NewLazySystemDLL("wtsapi32.dll")
modkernel32 *windows.LazyDLL = windows.NewLazySystemDLL("kernel32.dll")
modadvapi32 *windows.LazyDLL = windows.NewLazySystemDLL("advapi32.dll")
moduserenv *windows.LazyDLL = windows.NewLazySystemDLL("userenv.dll")
procWTSEnumerateSessionsW *windows.LazyProc = modwtsapi32.NewProc("WTSEnumerateSessionsW")
procWTSGetActiveConsoleSessionId *windows.LazyProc = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
procWTSQueryUserToken *windows.LazyProc = modwtsapi32.NewProc("WTSQueryUserToken")
procDuplicateTokenEx *windows.LazyProc = modadvapi32.NewProc("DuplicateTokenEx")
procCreateEnvironmentBlock *windows.LazyProc = moduserenv.NewProc("CreateEnvironmentBlock")
procCreateProcessAsUser *windows.LazyProc = modadvapi32.NewProc("CreateProcessAsUserW")
procGetTokenInformation *windows.LazyProc = modadvapi32.NewProc("GetTokenInformation")
)
type WTS_CONNECTSTATE_CLASS int
type SECURITY_IMPERSONATION_LEVEL int
type TOKEN_TYPE int
type SW int
type WTS_SESSION_INFO struct {
SessionID windows.Handle
WinStationName *uint16
State WTS_CONNECTSTATE_CLASS
}
type TOKEN_LINKED_TOKEN struct {
LinkedToken windows.Token
}
const (
WTS_CURRENT_SERVER_HANDLE uintptr = 0
)
const (
WTSActive WTS_CONNECTSTATE_CLASS = iota
WTSConnected
WTSConnectQuery
WTSShadow
WTSDisconnected
WTSIdle
WTSListen
WTSReset
WTSDown
WTSInit
)
const (
SecurityAnonymous SECURITY_IMPERSONATION_LEVEL = iota
SecurityIdentification
SecurityImpersonation
SecurityDelegation
)
const (
TokenPrimary TOKEN_TYPE = iota + 1
TokenImpersonazion
)
const (
SW_HIDE SW = 0
SW_SHOWNORMAL = 1
SW_NORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3
SW_MAXIMIZE = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_MINIMIZE = 6
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8
SW_RESTORE = 9
SW_SHOWDEFAULT = 10
SW_MAX = 1
)
const (
CREATE_UNICODE_ENVIRONMENT uint16 = 0x00000400
CREATE_NO_WINDOW = 0x08000000
CREATE_NEW_CONSOLE = 0x00000010
)
//获得当前系统活动的SessionID
func GetCurrentUserSessionId() (windows.Handle, error) {
sessionList, err := WTSEnumerateSessions()
if err != nil {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: %s", err)
}
for i := range sessionList {
if sessionList[i].State == WTSActive {
return sessionList[i].SessionID, nil
}
}
if sessionId, _, err := procWTSGetActiveConsoleSessionId.Call(); sessionId == 0xFFFFFFFF {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: call native WTSGetActiveConsoleSessionId: %s", err)
} else {
return windows.Handle(sessionId), nil
}
}
// WTSEnumerateSession will call the native
// version for Windows and parse the result
// to a Golang friendly version
func WTSEnumerateSessions() ([]*WTS_SESSION_INFO, error) {
var (
sessionInformation windows.Handle = windows.Handle(0)
sessionCount int = 0
sessionList []*WTS_SESSION_INFO = make([]*WTS_SESSION_INFO, 0)
)
if returnCode, _, err := procWTSEnumerateSessionsW.Call(WTS_CURRENT_SERVER_HANDLE, 0, 1, uintptr(unsafe.Pointer(&sessionInformation)), uintptr(unsafe.Pointer(&sessionCount))); returnCode == 0 {
return nil, fmt.Errorf("call native WTSEnumerateSessionsW: %s", err)
}
structSize := unsafe.Sizeof(WTS_SESSION_INFO{})
current := uintptr(sessionInformation)
for i := 0; i < sessionCount; i++ {
sessionList = append(sessionList, (*WTS_SESSION_INFO)(unsafe.Pointer(current)))
current += structSize
}
return sessionList, nil
}
// DuplicateUserTokenFromSessionID will attempt
// to duplicate the user token for the user logged
// into the provided session ID
func DuplicateUserTokenFromSessionID(sessionId windows.Handle, runas bool) (windows.Token, error) {
var (
impersonationToken windows.Handle = 0
userToken windows.Token = 0
)
if returnCode, _, err := procWTSQueryUserToken.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&impersonationToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native WTSQueryUserToken: %s", err)
}
if returnCode, _, err := procDuplicateTokenEx.Call(uintptr(impersonationToken), 0, 0, uintptr(SecurityImpersonation), uintptr(TokenPrimary), uintptr(unsafe.Pointer(&userToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native DuplicateTokenEx: %s", err)
}
if runas {
var admin TOKEN_LINKED_TOKEN
var dt uintptr = 0
if returnCode, _, _ := procGetTokenInformation.Call(uintptr(impersonationToken), 19, uintptr(unsafe.Pointer(&admin)), uintptr(unsafe.Sizeof(admin)), uintptr(unsafe.Pointer(&dt))); returnCode != 0 {
userToken = admin.LinkedToken
}
}
if err := windows.CloseHandle(impersonationToken); err != nil {
return 0xFFFFFFFF, fmt.Errorf("close windows handle used for token duplication: %s", err)
}
return userToken, nil
}
//StartProcessAsCurrentUser(程序路径, 启动参数, 工作目录 string, 是否以管理员身份运行) error
//需要注意的是,若使用cmdLine传入启动参数,则需要加上传入文件路径,否则可能会有不可预期的错误。
//例:
//
//StartProcessAsCurrentUser(`C:\test\test.exe`,`C:\test\test.exe hello world`,`C:\test`,true)
func StartProcessAsCurrentUser(appPath, cmdLine, workDir string, runas bool) error {
var (
sessionId windows.Handle
userToken windows.Token
envInfo windows.Handle
startupInfo windows.StartupInfo
processInfo windows.ProcessInformation
commandLine uintptr = 0
workingDir uintptr = 0
err error
)
if sessionId, err = GetCurrentUserSessionId(); err != nil {
return err
}
if userToken, err = DuplicateUserTokenFromSessionID(sessionId, runas); err != nil {
return fmt.Errorf("get duplicate user token for current user session: %s", err)
}
if returnCode, _, err := procCreateEnvironmentBlock.Call(uintptr(unsafe.Pointer(&envInfo)), uintptr(userToken), 0); returnCode == 0 {
return fmt.Errorf("create environment details for process: %s", err)
}
creationFlags := CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE
startupInfo.ShowWindow = SW_SHOW
startupInfo.Desktop = windows.StringToUTF16Ptr("winsta0\\default")
if len(cmdLine) > 0 {
commandLine = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(cmdLine)))
}
if len(workDir) > 0 {
workingDir = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(workDir)))
}
if returnCode, _, err := procCreateProcessAsUser.Call(
uintptr(userToken), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(appPath))), commandLine, 0, 0, 0,
uintptr(creationFlags), uintptr(envInfo), workingDir, uintptr(unsafe.Pointer(&startupInfo)), uintptr(unsafe.Pointer(&processInfo)),
); returnCode == 0 {
return fmt.Errorf("create process as user: %s", err)
}
return nil
}
至此service部分就基本完成了,赋予管理员权限(可选)。
另外,避免用户安装的时候未赋予管理员权限,可以通过修改NAC的方式,用于修改程序图标和权限的manifest。
service/nac.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
首先需要安装rsrc工具
要使rsrc生效,需要在windows下编译。
go get github.com/akavel/rsrc
针对无需ico图标,仅需要管理员权限的情况
rsrc -manifest nac.manifest -o nac.syso
针对需要ico图标的情况
rsrc -manifest nac.manifest -o nac.syso -ico x.ico
最后一步编译
mac下编译(不支持添加上述的图标manifest和UAC强制管理员权限)
需要安装mingw
brew install mingw-w64
编译64位
env CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc go build -ldflags="-w -s" -o EtaxService.exe service/*.go
编译32位(32位其实兼容性更好)
env CGO_ENABLED=1 GOOS=windows GOARCH=386 CC=i686-w64-mingw32-gcc go build -ldflags="-w -s" -o EtaxService.exe service/*.go
WINDOW下编译64位(windos下编译会自动检测到目录下的nac.syso文件,并打包manifest中的图标和UAC管理员权限配置)
service/build.bat
set CGO_ENABLED=1
set GOARCH=amd64
set GOOS=windows
go build -ldflags="-w -s" -o EtaxService.exe
pause
echo press any key continue
WINDOW下编译32位
service/build.bat
set CGO_ENABLED=1
set GOARCH=386
set GOOS=windows
go build -ldflags="-w -s" -o EtaxService.exe
pause
echo press any key continue
然后就是程序部分了
这部分没必要多做赘述,只是在编译的时候需要选择是否以无界面的方式后台运行。如果需要图标就重复上面windows添加图标的方法。
app/main.go
package main
func main() {
// 这里就自由发挥了
}
无界面编译app
go build -ldflags="-w -s -H windowsgui"
常规编译 (-ldflags="-w -s" 用于去除调试信息)
go build -ldflags="-w -s"