windows/svc: rewrite in Go
The old service management code was written in assembly and communicated
over Windows events, which resulted in non-obvious control flow.
NewCallback makes it possible to rewrite all of this in vanilla Go. This
also enables the service test on the Go builders, as modifying system
services shouldn't be an issue there.
Change-Id: I8003b57d11d4469f762058c648a4b7733530eeb8
Reviewed-on: https://go-review.googlesource.com/c/sys/+/330010
Trust: Jason A. Donenfeld <Jason@zx2c4.com>
Trust: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Jason A. Donenfeld <Jason@zx2c4.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/windows/service.go b/windows/service.go
index b269850..1a22b3e 100644
--- a/windows/service.go
+++ b/windows/service.go
@@ -235,3 +235,4 @@
//sys NotifyServiceStatusChange(service Handle, notifyMask uint32, notifier *SERVICE_NOTIFY) (ret error) = advapi32.NotifyServiceStatusChangeW
//sys SubscribeServiceChangeNotifications(service Handle, eventType uint32, callback uintptr, callbackCtx uintptr, subscription *uintptr) (ret error) = sechost.SubscribeServiceChangeNotifications?
//sys UnsubscribeServiceChangeNotifications(subscription uintptr) = sechost.UnsubscribeServiceChangeNotifications?
+//sys RegisterServiceCtrlHandlerEx(serviceName *uint16, handlerProc uintptr, context uintptr) (handle Handle, err error) = advapi32.RegisterServiceCtrlHandlerExW
diff --git a/windows/svc/event.go b/windows/svc/event.go
deleted file mode 100644
index 0508e22..0000000
--- a/windows/svc/event.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build windows
-
-package svc
-
-import (
- "errors"
-
- "golang.org/x/sys/windows"
-)
-
-// event represents auto-reset, initially non-signaled Windows event.
-// It is used to communicate between go and asm parts of this package.
-type event struct {
- h windows.Handle
-}
-
-func newEvent() (*event, error) {
- h, err := windows.CreateEvent(nil, 0, 0, nil)
- if err != nil {
- return nil, err
- }
- return &event{h: h}, nil
-}
-
-func (e *event) Close() error {
- return windows.CloseHandle(e.h)
-}
-
-func (e *event) Set() error {
- return windows.SetEvent(e.h)
-}
-
-func (e *event) Wait() error {
- s, err := windows.WaitForSingleObject(e.h, windows.INFINITE)
- switch s {
- case windows.WAIT_OBJECT_0:
- break
- case windows.WAIT_FAILED:
- return err
- default:
- return errors.New("unexpected result from WaitForSingleObject")
- }
- return nil
-}
diff --git a/windows/svc/go12.c b/windows/svc/go12.c
deleted file mode 100644
index 6f1be1f..0000000
--- a/windows/svc/go12.c
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build windows
-// +build !go1.3
-
-// copied from pkg/runtime
-typedef unsigned int uint32;
-typedef unsigned long long int uint64;
-#ifdef _64BIT
-typedef uint64 uintptr;
-#else
-typedef uint32 uintptr;
-#endif
-
-// from sys_386.s or sys_amd64.s
-void ·servicemain(void);
-
-void
-·getServiceMain(uintptr *r)
-{
- *r = (uintptr)·servicemain;
-}
diff --git a/windows/svc/go12.go b/windows/svc/go12.go
deleted file mode 100644
index cd8b913..0000000
--- a/windows/svc/go12.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build windows
-// +build !go1.3
-
-package svc
-
-// from go12.c
-func getServiceMain(r *uintptr)
diff --git a/windows/svc/go13.go b/windows/svc/go13.go
deleted file mode 100644
index 9d7f3ce..0000000
--- a/windows/svc/go13.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build windows
-// +build go1.3
-
-package svc
-
-import "unsafe"
-
-const ptrSize = 4 << (^uintptr(0) >> 63) // unsafe.Sizeof(uintptr(0)) but an ideal const
-
-// Should be a built-in for unsafe.Pointer?
-func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
- return unsafe.Pointer(uintptr(p) + x)
-}
-
-// funcPC returns the entry PC of the function f.
-// It assumes that f is a func value. Otherwise the behavior is undefined.
-func funcPC(f interface{}) uintptr {
- return **(**uintptr)(add(unsafe.Pointer(&f), ptrSize))
-}
-
-// from sys_386.s and sys_amd64.s
-func servicectlhandler(ctl uint32) uintptr
-func servicemain(argc uint32, argv **uint16)
-
-func getServiceMain(r *uintptr) {
- *r = funcPC(servicemain)
-}
diff --git a/windows/svc/service.go b/windows/svc/service.go
index 3748528..46c73a5 100644
--- a/windows/svc/service.go
+++ b/windows/svc/service.go
@@ -10,8 +10,7 @@
import (
"errors"
- "runtime"
- "syscall"
+ "sync"
"unsafe"
"golang.org/x/sys/internal/unsafeheader"
@@ -91,7 +90,6 @@
// Handler is the interface that must be implemented to build Windows service.
type Handler interface {
-
// Execute will be called by the package code at the start of
// the service, and the service will exit once Execute completes.
// Inside Execute you must read service change requests from r and
@@ -106,28 +104,6 @@
Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32)
}
-var (
- // These are used by asm code.
- goWaitsH uintptr
- cWaitsH uintptr
- ssHandle uintptr
- sName *uint16
- sArgc uintptr
- sArgv **uint16
- ctlHandlerExProc uintptr
- cSetEvent uintptr
- cWaitForSingleObject uintptr
- cRegisterServiceCtrlHandlerExW uintptr
-)
-
-func init() {
- k := windows.NewLazySystemDLL("kernel32.dll")
- cSetEvent = k.NewProc("SetEvent").Addr()
- cWaitForSingleObject = k.NewProc("WaitForSingleObject").Addr()
- a := windows.NewLazySystemDLL("advapi32.dll")
- cRegisterServiceCtrlHandlerExW = a.NewProc("RegisterServiceCtrlHandlerExW").Addr()
-}
-
type ctlEvent struct {
cmd Cmd
eventType uint32
@@ -140,36 +116,10 @@
type service struct {
name string
h windows.Handle
- cWaits *event
- goWaits *event
c chan ctlEvent
handler Handler
}
-func newService(name string, handler Handler) (*service, error) {
- var s service
- var err error
- s.name = name
- s.c = make(chan ctlEvent)
- s.handler = handler
- s.cWaits, err = newEvent()
- if err != nil {
- return nil, err
- }
- s.goWaits, err = newEvent()
- if err != nil {
- s.cWaits.Close()
- return nil, err
- }
- return &s, nil
-}
-
-func (s *service) close() error {
- s.cWaits.Close()
- s.goWaits.Close()
- return nil
-}
-
type exitCode struct {
isSvcSpecific bool
errno uint32
@@ -224,23 +174,43 @@
return windows.SetServiceStatus(s.h, &t)
}
-const (
- sysErrSetServiceStatusFailed = uint32(syscall.APPLICATION_ERROR) + iota
- sysErrNewThreadInCallback
+var (
+ initCallbacks sync.Once
+ ctlHandlerCallback uintptr
+ serviceMainCallback uintptr
)
-func (s *service) run() {
- s.goWaits.Wait()
- s.h = windows.Handle(ssHandle)
+func ctlHandler(ctl, evtype, evdata, context uintptr) uintptr {
+ s := (*service)(unsafe.Pointer(context))
+ e := ctlEvent{cmd: Cmd(ctl), eventType: uint32(evtype), eventData: evdata, context: 123456} // Set context to 123456 to test issue #25660.
+ s.c <- e
+ return 0
+}
- var argv []*uint16
- hdr := (*unsafeheader.Slice)(unsafe.Pointer(&argv))
- hdr.Data = unsafe.Pointer(sArgv)
- hdr.Len = int(sArgc)
- hdr.Cap = int(sArgc)
+var theService service // This is, unfortunately, a global, which means only one service per process.
- args := make([]string, len(argv))
- for i, a := range argv {
+// serviceMain is the entry point called by the service manager, registered earlier by
+// the call to StartServiceCtrlDispatcher.
+func serviceMain(argc uint32, argv **uint16) uintptr {
+ handle, err := windows.RegisterServiceCtrlHandlerEx(windows.StringToUTF16Ptr(theService.name), ctlHandlerCallback, uintptr(unsafe.Pointer(&theService)))
+ if sysErr, ok := err.(windows.Errno); ok {
+ return uintptr(sysErr)
+ } else if err != nil {
+ return uintptr(windows.ERROR_UNKNOWN_EXCEPTION)
+ }
+ theService.h = handle
+ defer func() {
+ theService.h = 0
+ windows.CloseHandle(handle)
+ }()
+ var args16 []*uint16
+ hdr := (*unsafeheader.Slice)(unsafe.Pointer(&args16))
+ hdr.Data = unsafe.Pointer(argv)
+ hdr.Len = int(argc)
+ hdr.Cap = int(argc)
+
+ args := make([]string, len(args16))
+ for i, a := range args16 {
args[i] = windows.UTF16PtrToString(a)
}
@@ -249,7 +219,7 @@
exitFromHandler := make(chan exitCode)
go func() {
- ss, errno := s.handler.Execute(args, cmdsToHandler, changesFromHandler)
+ ss, errno := theService.handler.Execute(args, cmdsToHandler, changesFromHandler)
exitFromHandler <- exitCode{ss, errno}
}()
@@ -258,7 +228,7 @@
CurrentStatus: Status{State: Stopped},
}
var outch chan ChangeRequest
- inch := s.c
+ inch := theService.c
loop:
for {
select {
@@ -274,14 +244,13 @@
outcr.EventData = r.eventData
outcr.Context = r.context
case outch <- outcr:
- inch = s.c
+ inch = theService.c
outch = nil
case c := <-changesFromHandler:
- err := s.updateStatus(&c, &ec)
+ err := theService.updateStatus(&c, &ec)
if err != nil {
- // best suitable error number
- ec.errno = sysErrSetServiceStatusFailed
- if err2, ok := err.(syscall.Errno); ok {
+ ec.errno = uint32(windows.ERROR_EXCEPTION_IN_SERVICE)
+ if err2, ok := err.(windows.Errno); ok {
ec.errno = uint32(err2)
}
break loop
@@ -292,87 +261,30 @@
}
}
- s.updateStatus(&Status{State: Stopped}, &ec)
- s.cWaits.Set()
-}
+ theService.updateStatus(&Status{State: Stopped}, &ec)
-func newCallback(fn interface{}) (cb uintptr, err error) {
- defer func() {
- r := recover()
- if r == nil {
- return
- }
- cb = 0
- switch v := r.(type) {
- case string:
- err = errors.New(v)
- case error:
- err = v
- default:
- err = errors.New("unexpected panic in syscall.NewCallback")
- }
- }()
- return syscall.NewCallback(fn), nil
+ return windows.NO_ERROR
}
-// BUG(brainman): There is no mechanism to run multiple services
-// inside one single executable. Perhaps, it can be overcome by
-// using RegisterServiceCtrlHandlerEx Windows api.
-
// Run executes service name by calling appropriate handler function.
func Run(name string, handler Handler) error {
- runtime.LockOSThread()
-
- tid := windows.GetCurrentThreadId()
-
- s, err := newService(name, handler)
- if err != nil {
- return err
- }
-
- ctlHandler := func(ctl, evtype, evdata, context uintptr) uintptr {
- e := ctlEvent{cmd: Cmd(ctl), eventType: uint32(evtype), eventData: evdata, context: context}
- // We assume that this callback function is running on
- // the same thread as Run. Nowhere in MS documentation
- // I could find statement to guarantee that. So putting
- // check here to verify, otherwise things will go bad
- // quickly, if ignored.
- i := windows.GetCurrentThreadId()
- if i != tid {
- e.errno = sysErrNewThreadInCallback
- }
- s.c <- e
- // Always return NO_ERROR (0) for now.
- return windows.NO_ERROR
- }
-
- var svcmain uintptr
- getServiceMain(&svcmain)
+ initCallbacks.Do(func() {
+ ctlHandlerCallback = windows.NewCallback(ctlHandler)
+ serviceMainCallback = windows.NewCallback(serviceMain)
+ })
+ theService.name = name
+ theService.handler = handler
+ theService.c = make(chan ctlEvent)
t := []windows.SERVICE_TABLE_ENTRY{
- {ServiceName: syscall.StringToUTF16Ptr(s.name), ServiceProc: svcmain},
+ {ServiceName: windows.StringToUTF16Ptr(theService.name), ServiceProc: serviceMainCallback},
{ServiceName: nil, ServiceProc: 0},
}
-
- goWaitsH = uintptr(s.goWaits.h)
- cWaitsH = uintptr(s.cWaits.h)
- sName = t[0].ServiceName
- ctlHandlerExProc, err = newCallback(ctlHandler)
- if err != nil {
- return err
- }
-
- go s.run()
-
- err = windows.StartServiceCtrlDispatcher(&t[0])
- if err != nil {
- return err
- }
- return nil
+ return windows.StartServiceCtrlDispatcher(&t[0])
}
// StatusHandle returns service status handle. It is safe to call this function
// from inside the Handler.Execute because then it is guaranteed to be set.
// This code will have to change once multiple services are possible per process.
func StatusHandle() windows.Handle {
- return windows.Handle(ssHandle)
+ return theService.h
}
diff --git a/windows/svc/svc_test.go b/windows/svc/svc_test.go
index e8684dd..f7833ad 100644
--- a/windows/svc/svc_test.go
+++ b/windows/svc/svc_test.go
@@ -77,7 +77,7 @@
}
func TestExample(t *testing.T) {
- if testing.Short() {
+ if testing.Short() && os.Getenv("GO_BUILDER_NAME") != "" {
t.Skip("skipping test in short mode - it modifies system services")
}
diff --git a/windows/svc/sys_windows_386.s b/windows/svc/sys_windows_386.s
deleted file mode 100644
index 1ed9141..0000000
--- a/windows/svc/sys_windows_386.s
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// func servicemain(argc uint32, argv **uint16)
-TEXT ·servicemain(SB),7,$0
- MOVL argc+0(FP), AX
- MOVL AX, ·sArgc(SB)
- MOVL argv+4(FP), AX
- MOVL AX, ·sArgv(SB)
-
- PUSHL BP
- PUSHL BX
- PUSHL SI
- PUSHL DI
-
- SUBL $12, SP
-
- MOVL ·sName(SB), AX
- MOVL AX, (SP)
- MOVL $·servicectlhandler(SB), AX
- MOVL AX, 4(SP)
- // Set context to 123456 to test issue #25660.
- MOVL $123456, 8(SP)
- MOVL ·cRegisterServiceCtrlHandlerExW(SB), AX
- MOVL SP, BP
- CALL AX
- MOVL BP, SP
- CMPL AX, $0
- JE exit
- MOVL AX, ·ssHandle(SB)
-
- MOVL ·goWaitsH(SB), AX
- MOVL AX, (SP)
- MOVL ·cSetEvent(SB), AX
- MOVL SP, BP
- CALL AX
- MOVL BP, SP
-
- MOVL ·cWaitsH(SB), AX
- MOVL AX, (SP)
- MOVL $-1, AX
- MOVL AX, 4(SP)
- MOVL ·cWaitForSingleObject(SB), AX
- MOVL SP, BP
- CALL AX
- MOVL BP, SP
-
-exit:
- ADDL $12, SP
-
- POPL DI
- POPL SI
- POPL BX
- POPL BP
-
- MOVL 0(SP), CX
- ADDL $12, SP
- JMP CX
-
-// I do not know why, but this seems to be the only way to call
-// ctlHandlerProc on Windows 7.
-
-// func servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr {
-TEXT ·servicectlhandler(SB),7,$0
- MOVL ·ctlHandlerExProc(SB), CX
- JMP CX
diff --git a/windows/svc/sys_windows_amd64.s b/windows/svc/sys_windows_amd64.s
deleted file mode 100644
index 1e5ef92..0000000
--- a/windows/svc/sys_windows_amd64.s
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// func servicemain(argc uint32, argv **uint16)
-TEXT ·servicemain(SB),7,$0
- MOVQ SP, AX
- ANDQ $~15, SP // alignment as per Windows requirement
- SUBQ $48, SP // room for SP and 4 args as per Windows requirement
- // plus one extra word to keep stack 16 bytes aligned
- MOVQ AX, 32(SP)
-
- MOVL CX, ·sArgc(SB)
- MOVQ DX, ·sArgv(SB)
-
- MOVQ ·sName(SB), CX
- MOVQ $·servicectlhandler(SB), DX
- // BUG(pastarmovj): Figure out a way to pass in context in R8.
- // Set context to 123456 to test issue #25660.
- MOVQ $123456, R8
- MOVQ ·cRegisterServiceCtrlHandlerExW(SB), AX
- CALL AX
- CMPQ AX, $0
- JE exit
- MOVQ AX, ·ssHandle(SB)
-
- MOVQ ·goWaitsH(SB), CX
- MOVQ ·cSetEvent(SB), AX
- CALL AX
-
- MOVQ ·cWaitsH(SB), CX
- MOVQ $4294967295, DX
- MOVQ ·cWaitForSingleObject(SB), AX
- CALL AX
-
-exit:
- MOVQ 32(SP), SP
- RET
-
-// I do not know why, but this seems to be the only way to call
-// ctlHandlerProc on Windows 7.
-
-// func ·servicectlhandler(ctl uint32, evtype uint32, evdata uintptr, context uintptr) uintptr {
-TEXT ·servicectlhandler(SB),7,$0
- MOVQ ·ctlHandlerExProc(SB), AX
- JMP AX
diff --git a/windows/svc/sys_windows_arm.s b/windows/svc/sys_windows_arm.s
deleted file mode 100644
index 360b86e..0000000
--- a/windows/svc/sys_windows_arm.s
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "textflag.h"
-
-// func servicemain(argc uint32, argv **uint16)
-TEXT ·servicemain(SB),NOSPLIT|NOFRAME,$0
- MOVM.DB.W [R4, R14], (R13) // push {r4, lr}
- MOVW R13, R4
- BIC $0x7, R13 // alignment for ABI
-
- MOVW R0, ·sArgc(SB)
- MOVW R1, ·sArgv(SB)
-
- MOVW ·sName(SB), R0
- MOVW ·ctlHandlerExProc(SB), R1
- MOVW $0, R2
- MOVW ·cRegisterServiceCtrlHandlerExW(SB), R3
- BL (R3)
- CMP $0, R0
- BEQ exit
- MOVW R0, ·ssHandle(SB)
-
- MOVW ·goWaitsH(SB), R0
- MOVW ·cSetEvent(SB), R1
- BL (R1)
-
- MOVW ·cWaitsH(SB), R0
- MOVW $-1, R1
- MOVW ·cWaitForSingleObject(SB), R2
- BL (R2)
-
-exit:
- MOVW R4, R13 // free extra stack space
- MOVM.IA.W (R13), [R4, R15] // pop {r4, pc}
diff --git a/windows/svc/sys_windows_arm64.s b/windows/svc/sys_windows_arm64.s
deleted file mode 100644
index 3ca540e..0000000
--- a/windows/svc/sys_windows_arm64.s
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-#include "textflag.h"
-
-// func servicemain(argc uint32, argv **uint16)
-TEXT ·servicemain(SB),NOSPLIT|NOFRAME,$0
- MOVD R0, ·sArgc(SB)
- MOVD R1, ·sArgv(SB)
-
- MOVD ·sName(SB), R0
- MOVD ·ctlHandlerExProc(SB), R1
- MOVD $0, R2
- MOVD ·cRegisterServiceCtrlHandlerExW(SB), R3
- BL (R3)
- CMP $0, R0
- BEQ exit
- MOVD R0, ·ssHandle(SB)
-
- MOVD ·goWaitsH(SB), R0
- MOVD ·cSetEvent(SB), R1
- BL (R1)
-
- MOVD ·cWaitsH(SB), R0
- MOVD $-1, R1
- MOVD ·cWaitForSingleObject(SB), R2
- BL (R2)
-
-exit:
- RET
diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go
index 4ea788e..1636013 100644
--- a/windows/zsyscall_windows.go
+++ b/windows/zsyscall_windows.go
@@ -124,6 +124,7 @@
procRegQueryInfoKeyW = modadvapi32.NewProc("RegQueryInfoKeyW")
procRegQueryValueExW = modadvapi32.NewProc("RegQueryValueExW")
procRegisterEventSourceW = modadvapi32.NewProc("RegisterEventSourceW")
+ procRegisterServiceCtrlHandlerExW = modadvapi32.NewProc("RegisterServiceCtrlHandlerExW")
procReportEventW = modadvapi32.NewProc("ReportEventW")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procSetEntriesInAclW = modadvapi32.NewProc("SetEntriesInAclW")
@@ -1055,6 +1056,15 @@
return
}
+func RegisterServiceCtrlHandlerEx(serviceName *uint16, handlerProc uintptr, context uintptr) (handle Handle, err error) {
+ r0, _, e1 := syscall.Syscall(procRegisterServiceCtrlHandlerExW.Addr(), 3, uintptr(unsafe.Pointer(serviceName)), uintptr(handlerProc), uintptr(context))
+ handle = Handle(r0)
+ if handle == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+
func ReportEvent(log Handle, etype uint16, category uint16, eventId uint32, usrSId uintptr, numStrings uint16, dataSize uint32, strings **uint16, rawData *byte) (err error) {
r1, _, e1 := syscall.Syscall9(procReportEventW.Addr(), 9, uintptr(log), uintptr(etype), uintptr(category), uintptr(eventId), uintptr(usrSId), uintptr(numStrings), uintptr(dataSize), uintptr(unsafe.Pointer(strings)), uintptr(unsafe.Pointer(rawData)))
if r1 == 0 {