25 July 2022
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
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)
}
}
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:
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)
}
}
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")
}