winsvc

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 15, 2023 License: Apache-2.0 Imports: 17 Imported by: 0

README

About the Go Windows Service module

This module makes it easier to implement Windows services in Go. It is based on the example service from https://golang.org/x/sys/windows/svc but unlike that example it is meant to be used as a library.

The module depends on https://gitlab.com/zygoon/go-cmdr and is in fact organized to allow nearly every cmdr.Cmd to run as a service. Proper services must respond to configuration commands, but writing one is much easier this way.

Unstable Module Warning

This module is not yet mature enough to offer API stability.

For more information about Go modules and API stability see https://go.dev/doc/modules/release-workflow#first-unstable

The module will be subsequently updated as the author gains experience with Windows services and attempts to avoid problematic API. This most notably affects the logger which does not integrate with services that write to standard IO streams.

Overview of the package

The entry point is the Wrapper type. It adapts an arbitrary function which takes a context and a slice of arguments to run as a service. Minimalistic configuration is required: the service must be given a system-wide unique name and configuration provided to the service manager.

Wrapper implements the cmdr.Cmd interface and can be passed to cmdr.RunMain inside the main function to set everything up. Wrapper offers a number of commands which are discussed below. Use Wrapper.ManagementFlags to tweak command selection if desired.

Env when given a context.Context returns the Environment which conveys service-specific communication channels as well as access to the event log. Error is an error capable of providing service-service specific return types from a regular cmdr.Cmd. In general ServiceReturn provides such values from any error. Both Environment and Error are meant to adapt the svc.Handler interface to the cmdr.Cmd interface.

Wrapper commands

When invoked outside of a Windows service, the wrapper offers the following sub-commands.

The install and remove commands require elevated permissions and allow to register the service to be managed by Windows. Any arguments passed to install are automatically provided to the service on startup.

The start and stop commands start and stop a service that was previously registered with install. When start is called, the service process start to execute. When stop is called the process (eventually) terminates.

The next set of commands offer particular interactions unique to the Windows service system. The pause command tells a running service to pause whatever the service may be doing. This does not cause the service process to terminate. The continue command asks the service to continue paused execution.

Lastly the debug command allows a service to run interactively in a terminal window. Windows services are not normally run this way but this is useful for development purposes.

Running commands as services

As mentioned, the intent of this module is to run any cmdr.Cmd as a service. Practical services need some additional code.

Go documentation contains an example service which "beeps" periodically. The following example shows how such a beeper, here named pinger, might look like.

The important novelty is that service-specific arguments are provided through the command context. The winsvc.Env function returns an Environment object with three fields.

There are two channels: Environment.S is a channel where the service can send status updates. Environment.R is a channel where the service must receive configuration commands. See golang.org/x/sys/windows/svc.Handler for details about how those channels are used.

Lastly, there is a Environment.L which is a windows service logger. The logger can log messages later available in the Windows Event Viewer.


func pinger(ctx context.Context, args []string) error {
	ev, _ := winsvc.Env(ctx)
	if ev == nil {
		return fmt.Errorf("pinger must run as a Windows service")
	}

	const acceptMask = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue

	ev.S <- svc.Status{State: svc.StartPending}

	fasttick := time.Tick(1 * time.Second)
	slowtick := time.Tick(5 * time.Second)
	tick := fasttick

	ev.S <- svc.Status{State: svc.Running, Accepts: acceptMask}

loop:
	for {
		select {
		case <-tick:
			ev.L.Info(1, "ping")

		case c := <-ev.R:
			ev.L.Info(1, fmt.Sprintf("received service command: %#v", c))
			switch c.Cmd {
			case svc.Interrogate:
				ev.S <- c.CurrentStatus

			case svc.Stop, svc.Shutdown:
				break loop

			case svc.Pause:
				ev.S <- svc.Status{State: svc.Paused, Accepts: acceptMask}
				tick = slowtick

			case svc.Continue:
				ev.S <- svc.Status{State: svc.Running, Accepts: acceptMask}
				tick = fasttick

			default:
				ev.L.Error(1, fmt.Sprintf("unexpected control request #%v", c))
			}
		}
	}

	ev.S <- svc.Status{State: svc.StopPending}

	return nil
}

To run the pinger as a service all you need is to instantiate and run the wrapper command.

func main() {
	cmd := winsvc.Wrapper{
		ServiceName: "pinger",
		ServiceCmd:  cmdr.Func(pinger),
		InstallConfig: mgr.Config{
			DisplayName: "Pinger demo service",
		},
	}

	cmdr.RunMain(&cmd, os.Args)
}

Contributions

Contributions are welcome. Please try to respect Go requirements (1.16 at the moment) and the overall coding and testing style.

License and REUSE

This project is licensed under the Apache 2.0 license, see the LICENSE file for details. The project is compliant with https://reuse.software/ - making it easy to ensure license and copyright compliance by automating software bill-of-materials. In other words, it's a good citizen in the modern free software stacks.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ServiceReturn

func ServiceReturn(err error) (isServiceSpecific bool, code uint32)

ServiceReturn computes service exit code for a given error.

Error instances carry both values directly. Any instances of syscall.Errno are treated as non-service-specific errors. All other non-nil errors return service-specific exit code 1.

func WithEnv

func WithEnv(parent context.Context, ev *Environment) context.Context

WithEnv returns a context with a windows service environment.

Types

type Environment

type Environment struct {
	// R is the channel where the system sends service change requests.
	R <-chan svc.ChangeRequest
	// C is the channel where the service can send updated status.
	S chan<- svc.Status
	// L is an interface for logging from Windows services. L is either a
	// debug.ConsoleLog or eventlog.Log depending on how the service is started.
	L debug.Log
}

Environment contains windows service command and control channels.

func Env

func Env(ctx context.Context) (*Environment, bool)

Env retrieves a windows service environment from a context.

type Error

type Error struct {
	IsServiceSpecific bool
	ExitCode          uint32
}

Error conveys windows service from a cmdr.Cmd error return.

func (*Error) Error

func (e *Error) Error() string

Error returns a message describing the error.

type ManagementFlags added in v0.1.3

type ManagementFlags uint32

ManagementFlags controls which of the management commands are enabled.

const (
	// SubsetMode indicates that not all management commands should be enabled.
	// When SubsetMode is not enabled then all remaining flags are ignored and
	// all commands are enabled automatically.
	SubsetMode ManagementFlags = 1 << iota
	// PauseAndContinue indicates that "pause" and "continue" commands should be
	// enabled.
	//
	// This only makes sense if the service is able to respond to the
	// corresponding commands and advertises support through the accept mask.
	PauseAndContinue
	// InstallAndRemove indicates that "install" and "remove" commands should be
	// enabled.
	InstallAndRemove
	// StartAndStop indicates that "start" and "stop" commands should be
	// enabled.
	StartAndStop
	// InteractiveDebug indicates that the "debug" command should be enabled.
	InteractiveDebug
)

type Wrapper

type Wrapper struct {
	ServiceName   string
	ServiceCmd    cmdr.Cmd
	InstallConfig mgr.Config

	// ManagementFlags controls which management commands are enabled.
	//
	// The zero value offers backwards compatibility where all commands are
	// enabled. To enable a subset use SubsetMode together with a mask
	// representing the desired commands.
	ManagementFlags ManagementFlags
}

Wrapper allows running commands as Windows services.

The wrapper offers a set of standard sub-commands for installing, removing, pausing, continuing and debugging a Windows service. The wrapped command must use Env to retrieve service command and control channels from the context.

While any command may be wrapped and ran as a service, commands should actively drain the command channel to behave properly.

func (*Wrapper) Run

func (cmd *Wrapper) Run(ctx context.Context, args []string) error

Run starts a Windows service or routes execution to management commands.

Depending on the execution environment, one of two things happens. When Run is called from a Windows service process, it runs the ServiceCmd command directly. When running outside of a Windows service, for example from an interactive terminal, Run offers a set of management commands.

The command can return any error. Errors posing as ServiceError can be used to convey service specific error codes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL