Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Menulis Perkhidmatan Windows dalam Go

Menulis Perkhidmatan Windows dalam Go

PHPz
PHPzasal
2024-08-28 06:40:101129semak imbas

Writing a Windows Service in Go

Jadual Kandungan

  • Pengenalan
  • Apakah sebenarnya "perkhidmatan" windows?
  • Kenapa Golang?
  • Menulis perkhidmatan windows dalam Go
  • Memasang dan Memulakan Perkhidmatan
  • Kesimpulan
  • Kod Lengkap

pengenalan

Hello Devs, Sudah lama saya tidak menulis sesuatu windows-ish. Jadi, hari ini saya ingin membimbing anda tentang cara menulis Aplikasi Perkhidmatan Windows dalam Go. Ya, anda mendengarnya dengan betul, itu bahasa pergi. Dalam blog tutorial ini, kami akan membincangkan beberapa perkara asas tentang Aplikasi Perkhidmatan Windows dan pada bahagian kemudiannya, saya akan membimbing anda melalui laluan kod mudah di mana kami menulis kod untuk perkhidmatan Windows yang log beberapa maklumat ke fail. Tanpa berlengah lagi, Jom mulakan...!

Apakah sebenarnya "perkhidmatan" windows?

Aplikasi Perkhidmatan Windows a.k.a. Perkhidmatan Windows ialah aplikasi kecil yang berjalan di latar belakang. Tidak seperti aplikasi windows biasa, mereka tidak mempunyai GUI atau sebarang bentuk antara muka pengguna. Aplikasi perkhidmatan ini mula dijalankan apabila komputer but. Ia berjalan tanpa mengira akaun pengguna yang mana ia dijalankan. Kitaran hayat (mula, berhenti, jeda, teruskan dll.) dikawal oleh program yang dipanggil Pengurus Kawalan Perkhidmatan (SCM).

Jadi, daripada ini, kami dapat memahami bahawa kami harus menulis Perkhidmatan Windows kami sedemikian rupa sehingga SCM harus berinteraksi dengan Perkhidmatan Windows kami dan mengurus kitaran hayatnya.

Kenapa Golang?

Terdapat beberapa faktor yang anda boleh pertimbangkan Go untuk menulis Perkhidmatan Windows.

Concurrency

Model konkurensi Go membolehkan pemprosesan yang lebih pantas dan cekap sumber. Go's goroutines membolehkan kami menulis aplikasi yang boleh melakukan pelbagai tugas tanpa sebarang sekatan atau kebuntuan.

Kesederhanaan

Secara tradisinya, Perkhidmatan Windows ditulis menggunakan sama ada C++ atau C (kadangkala C#) yang bukan sahaja menghasilkan kod yang kompleks, tetapi juga DX (Pengalaman Pembangun) yang lemah. Pelaksanaan Perkhidmatan Windows Go adalah mudah dan setiap baris kod masuk akal.

Perduaan Statik

Anda mungkin bertanya, "Mengapa tidak menggunakan bahasa yang lebih mudah seperti python?". Alasannya adalah kerana sifat Python yang ditafsirkan. Go menyusun ke binari fail tunggal yang dipautkan secara statik yang penting untuk Perkhidmatan Windows berfungsi dengan cekap. Perduaan Go tidak memerlukan sebarang masa jalan / penterjemah. Kod Go juga boleh disusun silang.

Akses Tahap Rendah

Walaupun sebagai bahasa yang Dikumpul Sampah, Go menyediakan sokongan padu untuk berinteraksi dengan elemen tahap rendah. Kami boleh menggunakan API win32 dan syscall am dengan mudah.

Baiklah, maklumat yang mencukupi. Jom kod...

Menulis perkhidmatan windows dalam Go

Laluan kod ini mengandaikan bahawa anda mempunyai pengetahuan asas tentang sintaks Go. Jika tidak, A Tour of Go adalah tempat yang bagus untuk mempelajarinya.

  • Pertama sekali, mari namakan projek kami. Saya akan menamakan lombong sebagai kosmik/perkhidmatan_saya. Buat fail go.mod,
PS C:\> go mod init cosmic/my_service
  • Sekarang kita perlu memasang pakej golang.org/x/sys. Pakej ini menyediakan sokongan bahasa go untuk aplikasi berkaitan Windows OS.
PS C:\> go get golang.org/x/sys

Nota: Pakej ini juga mengandungi sokongan bahasa tahap OS untuk OS berasaskan UNIX seperti Mac OS dan Linux.

  • Buat fail main.go. Fail main.go mengandungi fungsi utama, yang bertindak sebagai titik masuk untuk aplikasi/perkhidmatan Go kami.

  • Untuk mencipta contoh perkhidmatan, kita perlu menulis sesuatu yang dipanggil Konteks Perkhidmatan, yang melaksanakan antara muka Pengendali daripada golang.org/x/sys/windows/svc.

Jadi, definisi antara muka kelihatan seperti ini

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

Fungsi Laksanakan akan dipanggil oleh kod pakej pada permulaan perkhidmatan dan perkhidmatan akan keluar sebaik sahaja Laksana selesai.

Kami membaca permintaan pertukaran perkhidmatan daripada saluran r terima sahaja dan bertindak sewajarnya. Kami juga harus memastikan perkhidmatan kami dikemas kini dengan menghantar isyarat ke saluran hantar sahaja. Kita boleh menghantar argumen pilihan kepada parameter args.

Apabila keluar, kita boleh kembali dengan exitCode adalah 0 pada pelaksanaan yang berjaya. Kami juga boleh menggunakan svcSpecificEC untuk itu.

  • Sekarang, buat jenis bernama myService yang akan bertindak sebagai Konteks Perkhidmatan kami.
type myService struct{}
  • Selepas mencipta jenis myService, tambahkan Execute yang disebutkan di atas sebagai kaedah kepadanya supaya ia melaksanakan antara muka Pengendali.
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
    // to be filled
}
  • Sekarang kami telah berjaya melaksanakan antara muka Pengendali, kami kini boleh mula menulis logik sebenar.

Buat pemalar, dengan isyarat yang perkhidmatan kami boleh terima daripada 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)
}

Atas ialah kandungan terperinci Menulis Perkhidmatan Windows dalam Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn