Hello Devs, It's been a while since I wrote something windows-ish. So, today I want to guide you guys on how to write a Windows Service Application in Go. Yes, you heard it right, it's go language. In this tutorial blog, we'll cover some basic stuffs about Windows Service Applications and at the later part, I'll guide you through a simple code walk through where we write code for a Windows service which logs some information to a file. Without further ado, Let's get started...!
A Windows Service Application a.k.a. Windows Services are tiny applications that runs in background. Unlike normal windows applications, they don't have a GUI or any forms of user interface. These service applications starts to run when the computer boots up. It runs irrespective of which user account it's running in. It's lifecycle (start, stop, pause, continue etc.,) are controlled by a program called Service Control Manager (SCM).
So, from this we can understand that we should write our Windows Service in such a way that the SCM should interact with our Windows Service and manage it's lifecycle.
There are several factors where you may consider Go for writing Windows Services.
Go's concurrency model allows for faster and resource efficient processing. Go's goroutines allows us to write applications which can do multi-tasking without any blocking or deadlocking.
Traditionally, Windows Services are written using either C++ or C (sometimes C#) which not only results in a complex code, but also poor DX (Developer Experience). Go's implementation of Windows Services is straightforward and every line of codemakes sense.
You may ask, "Why not use even more simple language like python?". The reason is due to the interpreted nature of Python. Go compiles to a statically linked single file binary which is essential for a Windows Service to function efficiently. Go binaries doesn't require any runtime / interpreter. Go code can also be cross compiled.
Though being a Garbage Collected language, Go provides solid support to interact with low level elements. We can easily invoke win32 APIs and general syscalls in go.
Alright, enough information. Let's code...
This code walk-through assumes that you have a basic knowledge on Go syntax. If not, A Tour of Go would be a nice place to learn it.
PS C:\> go mod init cosmic/my_service
PS C:\> go get golang.org/x/sys
Note: This package also contains OS level go language support for UNIX based OS'es like Mac OS and Linux.
Create a main.go file. The main.go file contains main function, which acts as a entry-point for our Go application/service.
For creating a service instance, we need to write something calledService Context, which implements Handler interface from golang.org/x/sys/windows/svc.
So, the interface definition looks something like this
type Handler interface { Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32) }
Execute function will be called by the package code at the start of the service, and the service will exit once the Execute completes.
We read service change requests from receive-only channel r and act accordingly. We should also keep our service updated with sending signals to send-only channel s. We can pass optional arguments to args parameter.
On exiting, we can return with the exitCode being 0 on a successful execution. We can also use svcSpecificEC for that.
type myService struct{}
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { // to be filled }
Create a constant, with the signals that our service can accept from SCM.
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
Our main goal is the log some data every 30 seconds. So we need to define a thread safe timer for that.
tick := time.Tick(30 * time.Second)
So, we have done all the initialization stuffs. It's time to send START signal to the SCM. we're going to do exactly that,
status <- svc.Status{State: svc.StartPending} status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
Now we're going to write a loop which acts as amainloopfor our application. Handling events in loop makes our application never ending and we can break the loop only when the SCM sends STOP or SHUTDOWN signal.
loop: for { select { case <-tick: log.Print("Tick Handled...!") case c := <-r: switch c.Cmd { case svc.Interrogate: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: log.Print("Shutting service...!") break loop case svc.Pause: status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} case svc.Continue: status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} default: log.Printf("Unexpected service control request #%d", c) } } }
Here we used a select statement to receive signals from channels. In first case, we handle the Timer's tick signal. This case receives signal every 30 seconds, as we declared before. We log a string "Tick Handled...!" in this case.
Secondly, we handle the signals from SCM via the receive-only r channel. So, we assign the value of the signal from r to a variable c and using a switch statement, we can handle all the lifecycle event/signals of our service. We can see about each lifecycle below,
So, when on receiving either svc.Stop or svc.Shutdown signal, we break the loop. It is to be noted that we need to send STOP signal to the SCM to let the SCM know that our service is stopping.
status <- svc.Status{State: svc.StopPending} return false, 1
Note: It's super hard to debug Windows Service Applications when running on Service Control Mode. That's why we are writing an additional Debug mode.
func runService(name string, isDebug bool) { if isDebug { err := debug.Run(name, &myService{}) if err != nil { log.Fatalln("Error running service in debug mode.") } } else { err := svc.Run(name, &myService{}) if err != nil { log.Fatalln("Error running service in debug mode.") } } }
func main() { f, err := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalln(fmt.Errorf("error opening file: %v", err)) } defer f.Close() log.SetOutput(f) runService("myservice", false) //change to true to run in debug mode }
Note: We are logging the logs to a log file. In advanced scenarios, we log our logs to Windows Event Logger. (phew, that sounds like a tongue twister ?)
PS C:\> go build -ldflags "-s -w"
For installing, deleting, starting and stopping our service, we use an inbuilt tool called sc.exe
To install our service, run the following command in powershellas Administrator,
PS C:\> sc.exe create MyService
To start our service, run the following command,
PS C:\> sc.exe start MyService
To delete our service, run the following command,
PS C:\> sc.exe delete MyService
You can explore more commands, just type sc.exe without any arguments to see the available commands.
As we can see, implementing Windows Services in go is straightforward and requires minimal implementation. You can write your own windows services which acts as a web server and more. Thanks for reading and don't forget to drop a ❤️.
Here is the complete code for your reference.
// file: main.go package main import ( "fmt" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" "log" "os" "time" ) type myService struct{} func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue tick := time.Tick(5 * time.Second) status <- svc.Status{State: svc.StartPending} status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} loop: for { select { case <-tick: log.Print("Tick Handled...!") case c := <-r: switch c.Cmd { case svc.Interrogate: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: log.Print("Shutting service...!") break loop case svc.Pause: status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} case svc.Continue: status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} default: log.Printf("Unexpected service control request #%d", c) } } } status <- svc.Status{State: svc.StopPending} return false, 1 } func runService(name string, isDebug bool) { if isDebug { err := debug.Run(name, &myService{}) if err != nil { log.Fatalln("Error running service in debug mode.") } } else { err := svc.Run(name, &myService{}) if err != nil { log.Fatalln("Error running service in debug mode.") } } } func main() { f, err := os.OpenFile("E:/awesomeProject/debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalln(fmt.Errorf("error opening file: %v", err)) } defer f.Close() log.SetOutput(f) runService("myservice", false) }
The above is the detailed content of Writing a Windows Service in Go. For more information, please follow other related articles on the PHP Chinese website!