首頁 >後端開發 >Golang >用 Go 編寫 Windows 服務

用 Go 編寫 Windows 服務

PHPz
PHPz原創
2024-08-28 06:40:101134瀏覽

Writing a Windows Service in Go

目錄

  • 簡介
  • Windows「服務」到底是什麼?
  • 為什麼選擇 Golang?
  • 用 Go 寫 Windows 服務
  • 安裝並啟動服務
  • 結論
  • 完整程式碼

介紹

開發者們大家好,我已經有一段時間沒有寫一些 Windows 風格的東西了。所以,今天我想指導大家如何用 Go 寫 Windows 服務應用程式。是的,你沒有聽錯,就是go語言。在本教學部落格中,我們將介紹有關Windows 服務應用程式的一些基本內容,在後面的部分中,我將指導您完成一個簡單的程式碼演練,我們為Windows 服務編寫程式碼,將一些資訊記錄到文件中。話不多說,讓我們開始吧...!

Windows「服務」到底是什麼?

Windows 服務應用程式又稱為 Windows 服務是在背景執行的微型應用程式。與普通的 Windows 應用程式不同,它們沒有 GUI 或任何形式的使用者介面。這些服務應用程式在電腦啟動時開始運作。無論它在哪個用戶帳戶中運行,它都會運行。它的生命週期(啟動、停止、暫停、繼續等)由名為服務控制管理員 (SCM) 的程式控制。

因此,從這裡我們可以理解,我們應該以這樣的方式編寫我們的 Windows 服務,即 SCM 應該與我們的 Windows 服務互動並管理它的生命週期。

為什麼選擇 Golang?

您可以考慮使用 Go 來編寫 Windows 服務的幾個因素。

並發性

Go 的並發模型允許更快且資源高效的處理。 Go 的 goroutine 允許我們編寫可以執行多任務處理而不會出現任何阻塞或死鎖的應用程式。

簡單

傳統上,Windows 服務是使用 C++ 或 C(有時是 C#)編寫的,這不僅導致程式碼複雜,而且 DX(開發人員體驗)很差。 Go 對 Windows 服務的實作非常簡單,每一行程式碼都有意義

靜態二進位文件

你可能會問,「為什麼不使用像Python這樣比較簡單的語言呢?」。原因是Python 的解釋性質。 Go 編譯為靜態連結的單一文件二進位文件,這對於 Windows 服務高效運行至關重要。 Go 二進位檔案不需要任何執行時間/解釋器。 Go程式碼也可以交叉編譯。

低級訪問

雖然 Go 是一種垃圾收集語言,但它為與低級元素互動提供了堅實的支援。我們可以在go中輕鬆呼叫win32 API和通用系統呼叫。

好的,資訊夠了。讓我們編碼...

用 Go 編寫 Windows 服務

此程式碼演練假設您具有 Go 語法的基本知識。如果沒有,A Tour of Go 將是一個學習 Go 的好地方。

  • 首先,讓我們為我們的專案命名。我將地雷命名為 cosmic/my_service。建立一個 go.mod 文件,
PS C:\> go mod init cosmic/my_service
  • 現在我們需要安裝 golang.org/x/sys 套件。該套件為Windows作業系統相關應用程式提供go語言支援。
PS C:\> go get golang.org/x/sys

注意:此軟體包還包含對基於 UNIX 的作業系統(例如 Mac OS 和 Linux)的作業系統級 Go 語言支援。

  • 建立一個 main.go 檔案。 main.go 檔案包含 main 函數,它充當我們的 Go 應用程式/服務的入口點。

  • 為了建立服務實例,我們需要寫一個名為 Service Context 的東西,它實作了 golang.org/x/sys/windows/svc 中的 Handler 介面。

所以,介面定義看起來像這樣

type Handler interface {
    Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32)
}

執行函數將在服務啟動時由套件代碼調用,一旦執行完成,服務將退出。

我們從僅接收通道 r 讀取服務變更請求並採取相應行動。我們還應該透過向僅發送通道發送信號來更新我們的服務。我們可以將可選參數傳遞給 args 參數。

退出時,如果執行成功,我們可以傳回 exitCode 為 0 的值。我們也可以使用 svcSpecificEC 來實現這一點。

  • 現在,建立一個名為 myService 的類型,它將充當我們的服務上下文。
type myService struct{}
  • 建立類型 myService 後,將上述的 Execute 作為方法加入其中,使其實作 Handler 介面。
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
    // to be filled
}
  • 現在我們已經成功實作了Handler接口,現在我們可以開始編寫實際的邏輯了。

建立一個常數,其中包含我們的服務可以從 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 a mainloop for 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,

  1. svc.Interrogate - Signal requested by SCM on a timely fashion to check the current status of the service.
  2. svc.Stop and svc.Shutdown - Signal sent by SCM when our service needs to be stopped or Shut Down.
  3. svc.Pause - Signal sent by SCM to pause the service execution without shutting it down.
  4. svc.Continue - Signal sent by SCM to resume the paused execution state of the service.

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
  • Now we write a function called runService where we enable our service to run either in Debug mode or in Service Control Mode.

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.")
        }
    }
}
  • Finally we can call the runService function in our main function.
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 ?)

  • Now run go build to create a binary '.exe'. Optionally, we can optimize and reduce the binary file size by using the following command,
PS C:\> go build -ldflags "-s -w"

Installing and Starting the Service

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 powershell as Administrator,

PS C:\> sc.exe create MyService <path to your service_app.exe>

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.

Conclusion

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 ❤️.

Complete Code

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)
}

以上是用 Go 編寫 Windows 服務的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn