blob: ee3d6965a180b9a3f2014657b3c41d9ab3ddf4ee [file] [log] [blame]
// 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 provides everything required to build Windows service.
//
package svc
import (
"errors"
"runtime"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// State describes service execution state (Stopped, Running and so on).
type State uint32
const (
Stopped = State(windows.SERVICE_STOPPED)
StartPending = State(windows.SERVICE_START_PENDING)
StopPending = State(windows.SERVICE_STOP_PENDING)
Running = State(windows.SERVICE_RUNNING)
ContinuePending = State(windows.SERVICE_CONTINUE_PENDING)
PausePending = State(windows.SERVICE_PAUSE_PENDING)
Paused = State(windows.SERVICE_PAUSED)
)
// Cmd represents service state change request. It is sent to a service
// by the service manager, and should be actioned upon by the service.
type Cmd uint32
const (
Stop = Cmd(windows.SERVICE_CONTROL_STOP)
Pause = Cmd(windows.SERVICE_CONTROL_PAUSE)
Continue = Cmd(windows.SERVICE_CONTROL_CONTINUE)
Interrogate = Cmd(windows.SERVICE_CONTROL_INTERROGATE)
Shutdown = Cmd(windows.SERVICE_CONTROL_SHUTDOWN)
ParamChange = Cmd(windows.SERVICE_CONTROL_PARAMCHANGE)
NetBindAdd = Cmd(windows.SERVICE_CONTROL_NETBINDADD)
NetBindRemove = Cmd(windows.SERVICE_CONTROL_NETBINDREMOVE)
NetBindEnable = Cmd(windows.SERVICE_CONTROL_NETBINDENABLE)
NetBindDisable = Cmd(windows.SERVICE_CONTROL_NETBINDDISABLE)
DeviceEvent = Cmd(windows.SERVICE_CONTROL_DEVICEEVENT)
HardwareProfileChange = Cmd(windows.SERVICE_CONTROL_HARDWAREPROFILECHANGE)
PowerEvent = Cmd(windows.SERVICE_CONTROL_POWEREVENT)
SessionChange = Cmd(windows.SERVICE_CONTROL_SESSIONCHANGE)
)
// Accepted is used to describe commands accepted by the service.
// Note that Interrogate is always accepted.
type Accepted uint32
const (
AcceptStop = Accepted(windows.SERVICE_ACCEPT_STOP)
AcceptShutdown = Accepted(windows.SERVICE_ACCEPT_SHUTDOWN)
AcceptPauseAndContinue = Accepted(windows.SERVICE_ACCEPT_PAUSE_CONTINUE)
AcceptParamChange = Accepted(windows.SERVICE_ACCEPT_PARAMCHANGE)
AcceptNetBindChange = Accepted(windows.SERVICE_ACCEPT_NETBINDCHANGE)
AcceptHardwareProfileChange = Accepted(windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE)
AcceptPowerEvent = Accepted(windows.SERVICE_ACCEPT_POWEREVENT)
AcceptSessionChange = Accepted(windows.SERVICE_ACCEPT_SESSIONCHANGE)
)
// Status combines State and Accepted commands to fully describe running service.
type Status struct {
State State
Accepts Accepted
CheckPoint uint32 // used to report progress during a lengthy operation
WaitHint uint32 // estimated time required for a pending operation, in milliseconds
ProcessId uint32 // if the service is running, the process identifier of it, and otherwise zero
}
// ChangeRequest is sent to the service Handler to request service status change.
type ChangeRequest struct {
Cmd Cmd
EventType uint32
EventData uintptr
CurrentStatus Status
Context uintptr
}
// 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
// act accordingly. You must keep service control manager up to date
// about state of your service by writing into s as required.
// args contains service name followed by argument strings passed
// to the service.
// You can provide service exit code in exitCode return parameter,
// with 0 being "no error". You can also indicate if exit code,
// if any, is service specific or not by using svcSpecificEC
// parameter.
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
eventData uintptr
context uintptr
errno uint32
}
// service provides access to windows service api.
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
}
func (s *service) updateStatus(status *Status, ec *exitCode) error {
if s.h == 0 {
return errors.New("updateStatus with no service status handle")
}
var t windows.SERVICE_STATUS
t.ServiceType = windows.SERVICE_WIN32_OWN_PROCESS
t.CurrentState = uint32(status.State)
if status.Accepts&AcceptStop != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_STOP
}
if status.Accepts&AcceptShutdown != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_SHUTDOWN
}
if status.Accepts&AcceptPauseAndContinue != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_PAUSE_CONTINUE
}
if status.Accepts&AcceptParamChange != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_PARAMCHANGE
}
if status.Accepts&AcceptNetBindChange != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_NETBINDCHANGE
}
if status.Accepts&AcceptHardwareProfileChange != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_HARDWAREPROFILECHANGE
}
if status.Accepts&AcceptPowerEvent != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_POWEREVENT
}
if status.Accepts&AcceptSessionChange != 0 {
t.ControlsAccepted |= windows.SERVICE_ACCEPT_SESSIONCHANGE
}
if ec.errno == 0 {
t.Win32ExitCode = windows.NO_ERROR
t.ServiceSpecificExitCode = windows.NO_ERROR
} else if ec.isSvcSpecific {
t.Win32ExitCode = uint32(windows.ERROR_SERVICE_SPECIFIC_ERROR)
t.ServiceSpecificExitCode = ec.errno
} else {
t.Win32ExitCode = ec.errno
t.ServiceSpecificExitCode = windows.NO_ERROR
}
t.CheckPoint = status.CheckPoint
t.WaitHint = status.WaitHint
return windows.SetServiceStatus(s.h, &t)
}
const (
sysErrSetServiceStatusFailed = uint32(syscall.APPLICATION_ERROR) + iota
sysErrNewThreadInCallback
)
func (s *service) run() {
s.goWaits.Wait()
s.h = windows.Handle(ssHandle)
argv := (*[100]*int16)(unsafe.Pointer(sArgv))[:sArgc]
args := make([]string, len(argv))
for i, a := range argv {
args[i] = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(a))[:])
}
cmdsToHandler := make(chan ChangeRequest)
changesFromHandler := make(chan Status)
exitFromHandler := make(chan exitCode)
go func() {
ss, errno := s.handler.Execute(args, cmdsToHandler, changesFromHandler)
exitFromHandler <- exitCode{ss, errno}
}()
ec := exitCode{isSvcSpecific: true, errno: 0}
outcr := ChangeRequest{
CurrentStatus: Status{State: Stopped},
}
var outch chan ChangeRequest
inch := s.c
loop:
for {
select {
case r := <-inch:
if r.errno != 0 {
ec.errno = r.errno
break loop
}
inch = nil
outch = cmdsToHandler
outcr.Cmd = r.cmd
outcr.EventType = r.eventType
outcr.EventData = r.eventData
outcr.Context = r.context
case outch <- outcr:
inch = s.c
outch = nil
case c := <-changesFromHandler:
err := s.updateStatus(&c, &ec)
if err != nil {
// best suitable error number
ec.errno = sysErrSetServiceStatusFailed
if err2, ok := err.(syscall.Errno); ok {
ec.errno = uint32(err2)
}
break loop
}
outcr.CurrentStatus = c
case ec = <-exitFromHandler:
break loop
}
}
s.updateStatus(&Status{State: Stopped}, &ec)
s.cWaits.Set()
}
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
}
// 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)
t := []windows.SERVICE_TABLE_ENTRY{
{ServiceName: syscall.StringToUTF16Ptr(s.name), ServiceProc: svcmain},
{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
}
// 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)
}