作為一個深度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"