Levin's blog

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:

Services tool screenshot with sample-service
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")