Writing a Windows Service in Go, Part 2
Part 1 of this series showed how to write a Windows service in Go by stepping through the source code of a very basic service. In this second part I want to show how to write a command-line program that can install, update, or uninstall the service.
We'll again use the golang.org/x/sys/windows/svc package, plus the golang.org/x/sys/windows/svc/mgr package which implements the functionality for installing, uninstalling, starting, and stopping services. The full source code is here: main.go
The command-line interface
Before we get started on the main function, let's define a function to print a "usage" message that explains how to use the installer on the command line. It's pretty straight-forward:
func usage(name string) {
fmt.Printf("Usage:\n")
fmt.Printf(" %s -i install service\n", name)
fmt.Printf(" %s -r remove service\n", name)
fmt.Printf(" %s -u update service\n", name)
os.Exit(1)
}
To keep things simple, we'll assume that the installer and service
.exe
files are in the same directory, so the first thing the main
function needs to do is look up the installer's own location in the file
system:
func main() {
executable, err := os.Executable()
if err != nil {
log.Fatalf("error getting executable: %v", err)
}
dir := filepath.Dir(executable)
Then, it checks the command-line arguments and calls a function that implements the requested functionality:
name, args := os.Args[0], os.Args[1:]
if len(args) != 1 {
usage(name)
}
switch args[0] {
case "-i":
install(dir)
case "-r":
remove()
case "-u":
update(dir)
default:
usage(name)
}
Installing the service
The install
function is where we start using the mgr
package. The
first step is always calling Connect
to connect to the Windows service
manager:
func install(dir string) {
m, err := mgr.Connect()
if err != nil {
log.Fatalf("error connecting to service control manager: %v", err)
}
Next, we create a Config to specify that we want the service to start automatically when Windows starts, and to set the name shown in the Services UI:
servicePath := filepath.Join(dir, "service.exe")
config := mgr.Config{
DisplayName: "Sample Windows service written in Go",
StartType: mgr.StartAutomatic,
}
Then create the new service:
s, err := m.CreateService("sample-service", servicePath, config)
if err != nil {
log.Fatalf("error creating service: %v", err)
}
defer s.Close()
CreateService
returns a
Service
object, which serves as a handle to the service. It'll be closed when
the function returns, but first we'll use it to start the new service:
err = s.Start()
if err != nil {
log.Fatalf("error starting service: %v", err)
}
Et voilà, the service is up and running:
Uninstalling the service
To uninstall the service, we again connect to the service control manager:
func remove() {
m, err := mgr.Connect()
if err != nil {
log.Fatalf("error connecting to service control manager: %v", err)
}
Then call OpenService
to get a Service
object:
s, err := m.OpenService("sample-service")
if err != nil {
log.Fatalf("error opening service: %v", err)
}
defer s.Close()
Then call Delete
-- this won't delete the service immediately, but it
marks it to be deleted as soon as it stops:
err = s.Delete()
if err != nil {
log.Fatalf("error marking service for deletion: %v", err)
}
Finally, send the Stop
signal to the service directly so it can shut
down properly:
_, err = s.Control(svc.Stop)
if err != nil {
log.Fatalf("error requesting service to stop: %v", err)
}
Updating the service
Let's say we've come out with a new and improved version 2 of the
service. We want the user to be able to do the update with a simple
.\installer.exe -u
on the Windows command line. Fortunately, the
mgr
package has everything we need. We'll start by connecting to the
Windows service control manager and getting the Service
object:
func update(dir string) {
log.Printf("updating service to version 2...")
m, err := mgr.Connect()
if err != nil {
log.Fatalf("error connecting to service control manager: %v", err)
}
service, err := m.OpenService("sample-service")
if err != nil {
log.Fatalf("error accessing service: %v", err)
}
For this sample code, we'll assume the new version is a file called
service-2.exe
. To switch to that new file, we prepare a new Config
that points to service-2.exe
and call UpdateConfig
:
config, err := service.Config()
if err != nil {
log.Fatalf("error getting service config: %v", err)
}
config.BinaryPathName = filepath.Join(dir, "service-2.exe")
err = service.UpdateConfig(config)
if err != nil {
log.Fatalf("error updating config: %v", err)
}
The next time Windows restarts it'll start the new version. If that's good enough for your service you can call it a day here, but in most cases we'll want to start the new version immediately. How do we do that? There's no Restart method, so we need to stop the service, then start it again.
But we don't have a Stop method either, all we have is a way to send a
Stop
request to the service. In the delete
function above we didn't
worry about that; we just sent the Stop
request and trusted that it
would indeed stop. For the update function we need to make sure it has
actually stopped before we can start it again. To do that we Query
the
service in a loop until it returns state Stopped
:
log.Print("requesting service to stop")
status, err := service.Control(svc.Stop)
if err != nil {
log.Fatalf("error requesting service to stop: %v", err)
}
log.Printf("sent stop; service state is %v", status.State)
for i := 0; i <= 12; i++ {
log.Printf("querying service status (attempt %d)", i)
status, err = service.Query()
if err != nil {
log.Fatalf("error querying service: %v", err)
}
log.Printf("service state: %v", status.State)
if status.State == svc.Stopped {
log.Println("service state is 'stopped'")
break
}
time.Sleep(10 * time.Second)
}
The code here will wait up to 2 minutes for the service to shut down, which should be plenty for the simple service from part 1. However, if your service can be slow to shut down -- maybe it needs to wait for some request to complete before it can exit cleanly -- you might want to increase those numbers.
Finally, starting the new service is the same as in the install
function:
log.Println("starting service")
err = service.Start()
if err != nil {
log.Fatalf("error starting service: %v", err)
}
log.Print("service updated to version 2")