這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。從 1999 年那時開始我就為 windows 寫過服務,一開始用 C/C++,後來用 C#。現在我在 Linux 上用 Go 編寫服務端軟體。然而我沒轍了。更令人沮喪的是,我一開始編寫軟體所用的作業系統並不是我即將部署所用的作業系統。當然,那是後話了。我想要在我的 Mac 上以後台進程(守護進程)的方式運行代碼。而我的問題也是,我無從下手。我很幸運在 Bitbucket 上找到了由 Daniel Theophanes 編寫的名為 [service](https://bitbucket.org/kardianos/service/src) 的開源項目。通過它的代碼我學會了如何在 Mac OS 上建立,安裝,啟動和停止守護進程。當然,這個項目也支援 Linux 和 Windows。## Mac OS 上的後台進程Mac OS 上有兩種類型的後台進程。守護進程(Daemons)和代理進程(Agents)。解釋如下:**守護進程** 作為整個系統的一部分運行在後台(也就是說,它並不和某個特定使用者關聯)。守護進程沒有圖形化介面,甚至不允許串連視窗伺服器(window server,並非指 Microsoft 的 Windows)。Web 服務器就是一個典型的例子。**代理進程** 則不同,它在後台以特定使用者的身份運行。它能做很多守護進程做不到的事,比如可靠地訪問使用者的主目錄或者串連視窗伺服器。更多資訊,訪問:[http://developer.apple.com/library/mac/#documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html](http://developer.apple.com/library/mac/#documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html)我們來看看如何在 Mac OS 上配置一個守護進程吧。![mac-os-daemons](https://raw.githubusercontent.com/studygolang/gctt-images/master/background-process/Screen-Shot-2013-07-28-at-9.51.35-AM.png)開啟 Finder 你可以找到如下目錄。Library 下的 LaunchDaemons 目錄是用來給我們存放 launchd .plist 檔案的。/System 下面也有一個 Library/LaunchDaemons 目錄,它則用來為作業系統的守護進程服務。在 Mac OS 上,launchd 程式是用來管理(包括啟動和停止)守護進程,應用程式,進程和指令碼的一個服務管理架構。一旦核心啟動了 launchd 程式,它就會開始掃描系統上的一系列目錄,包括 /etc 下的指令碼,/Library 和 /System/Library 下的 LaunchAgents,LaunchDaemons 目錄。LaunchDaemons 下找到的程式會以 root 的身份運行。這是一份 launchd .plist 檔案的範例,包含了基本的配置資訊:```xml<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\" ><plist version='1.0'><dict> <key>Label</key><string>My Service</string> <key>ProgramArguments</key> <array> <string>/Users/bill/MyService/MyService</string> </array> <key>WorkingDirectory</key><string>/Users/bill/MyService</string> <key>StandardOutPath</key><string>/Users/bill/MyService/My.log</string> <key>KeepAlive</key><true/> <key>Disabled</key><false/></dict></plist>```這裡你能找到 .plist 檔案中各種不同的配置項:[https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html)ProgramArguments 欄位非常重要:```xml<key>ProgramArguments</key><array><string>/Users/bill/MyService/MyService</string></array>```通過這個欄位,你能夠指定啟動並執行程式和傳遞給該程式 main 方法所需的其他參數。WorkingDirectory 和 StandardOutPath 兩個欄位也同樣很有用:```xml<key>WorkingDirectory</key><string>/Users/bill/MyService</string><key>StandardOutPath</key><string>/Users/bill/MyService/My.log</string>```有了 launchd .plist 檔案之後,我們就可以通過一個叫 launchctl 的程式來讓我們的程式以守護進程的方式啟動。```launchctl load /Library/LaunchDaemons/MyService.plist```launchctl 程式提供了服務控制和報告的功能。load 命令用以基於 launchd .plist 啟動一個守護進程。要驗證一下程式是否已經成功啟動可以使用 list 命令:```launchctl listPID Status Label948 - 0x7ff4a9503410.anonymous.launchctl946 - My Service910 - 0x7ff4a942ce00.anonymous.bash```可以看到,My Service 正在運行,PID 為 946。現在要停止程式可以用 unload 命令:```launchctl unload /Library/LaunchDaemons/MyService.plistlaunchctl listPID Status Label948 - 0x7ff4a9503410.anonymous.launchctl910 - 0x7ff4a942ce00.anonymous.bash```程式被終止了。我們還需要處理一下程式在啟動和終止的時候作業系統發送的啟動和停止請求。## 作業系統相關的 Go 檔案你可以編寫只針對特定平台的 Go 檔案。![go-specific-platform](https://raw.githubusercontent.com/studygolang/gctt-images/master/background-process/Screen-Shot-2013-07-28-at-9.52.55-AM.png)在我的 LiteIDE 項目中你可以看到 5 個 Go 源檔案。其中有 3 個檔案名稱中包含了其針對的目標平台的名字(darwin (Mac),linux 和 windows)。因為我現在是在 Mac OS 下進行構建,所以 `service_linux.go` 和 `service_windows.go` 兩個檔案會被編譯器忽略。編譯器預設就能識別這種命名規範。這是個很酷的特性,因為在不同的平台上總是要處理一些不同的事,使用一些不同的包。比如在 `service_windows.go` 檔案中,就引用了下面這些:```"code.google.com/p/winsvc/eventlog""code.google.com/p/winsvc/mgr""code.google.com/p/winsvc/svc"```目前我並沒有安裝這些包,因為我並不打算在 windows 上運行它。但這並不影響構建因為 `service_windows.go` 被忽略了。這還有另一個好處。因為這些檔案中只有一個會被編譯,所以我可以複用這些檔案中的類型和方法名。也就是說,任何使用這個包的代碼在更改平台之後也不需要做任何修改。實在是酷。## 服務介面每個服務為了提供命令和控制功能都必須實現三個介面。```gotype Service interface {InstallerControllerRunner}type Installer interface {Install(config *Config) errorRemove() error}type Controller interface {Start() errorStop() error} type Runner interface {Run(config *Config) error}```Installer 介面提供了在特定的作業系統上安裝和卸載後台進程的邏輯。Controller 介面提供了從命令列啟動和停止程式的邏輯。最後 Runner 介面是用來實現當被請求的時候作為服務執行的所有應用邏輯。## Darwin 下的實現既然這篇文章是針對 Mac OS 的,我就專註於說明 service_darwin.go 源檔案的實現。Installer 介面需要實現兩個方法,Install 和 Remove。按照上文所說,我們需要為服務建立一個 launchd .plist 檔案。要完成這步最好的方式是使用 text/template 包。_InstallScript 函數使用了一個多行的字串(字串面值)來建立 launchd .plist 檔案的模版。```gofunc _InstallScript() (script string) {return `<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"[ http://www.apple.com/DTDs/PropertyList-1.0.dtd ](../../broken-link.html)\" ><plist version='1.0'><dict> <key>Label</key><string><b>{{.DisplayName}}</b></string> <key>ProgramArguments</key> <array><string><b>{{.WorkingDirectory}}</b>/<b>{{.ExecutableName}}</b></string> </array> <key>WorkingDirectory</key><string><b>{{.WorkingDirectory}}</b></string> <key>StandardOutPath</key><string><b>{{.LogLocation}}</b>/<b>{{.Name}}</b>.log</string> <key>KeepAlive</key><true/> <key>Disabled</key><false/></dict></plist>`}```多行字串很酷的一點是其中的斷行符號,換行和空格也不會被忽略。因為這隻是個模版,所以我們需要讓其中的變數能夠被真實的資料替換。{{.variable_name}} 運算式用來定義這些變數。下面是 Install 方法的實現:```gofunc (service *_DarwinLaunchdService) Install(config *Config) error {confPath := service._GetServiceFilePath()_, err := os.Stat(confPath)if err == nil {return fmt.Errorf("Init already exists: %s", confPath)}file, err := os.Create(confPath)if err != nil {return err}defer file.Close()parameters := struct {ExecutableName stringWorkingDirectory stringName stringDisplayName stringLongDescription stringLogLocation string}{service._Config.ExecutableName,service._Config.WorkingDirectory,service._Config.Name,service._Config.DisplayName,service._Config.LongDescription,service._Config.LogLocation,}template := template.Must(template.New("launchdConfig").Parse(_InstallScript()))return template.Execute(file, ¶meters)}````_GetServiceFilePath()` 函數用來在不同的平台下都能擷取到設定檔的路徑。Darwin 下是這樣的:```gofunc (service *_DarwinLaunchdService) _GetServiceFilePath() string {return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", service._Config.Name)}```然後代碼會檢查檔案是否已經存在,不存在則會建立一個空檔案。緊接著我們建立一個結構體並填充好 template.Execute 函數調用需要的各項參數。注意欄位的名稱要和模版中 `{{.variable_name}}` 變數的名稱匹配。Execute 函數會處理好模版並將其寫入磁碟檔案。Controller 介面需要兩個方法,Start 和 Stop。在 Darwin 中代碼實現很簡單:```gofunc (service *_DarwinLaunchdService) Start() error {confPath := service._GetServiceFilePath()cmd := exec.Command("launchctl", "load", confPath)return cmd.Run()}func (service *_DarwinLaunchdService) Stop() error {confPath := service._GetServiceFilePath()cmd := exec.Command("launchctl", "unload", confPath)return cmd.Run()}```如同我們前面所說的一樣,每個方法都會調用 launchctl 程式。這是個啟動和停止守護進程的簡便方法。最後要實現的 Runner 介面只有一個叫 Run 的方法。```gofunc (service *_DarwinLaunchdService) Run(config *Config) error {defer func() {if r := recover(); r != nil {fmt.Println("******> SERVICE PANIC:", r)}}()fmt.Print("******> Initing Service\n")if config.Init != nil {if err := config.Init(); err != nil {return err}}fmt.Print("******> Starting Service\n")if config.Start != nil {if err := config.Start(); err != nil {return err}}fmt.Print("******> Service Started\n")// Create a channel to talk with the OSvar sigChan = make(chan os.Signal, 1)signal.Notify(sigChan, os.Interrupt)// Wait for an eventwhatSig := <-sigChanfmt.Print("******> Service Shutting Down\n")if config.Stop != nil {if err := config.Stop(); err != nil {return err}}fmt.Print("******> Service Down\n")return nil}```當程式開始以守護進程的方式運行時 Run 方法就會被調用。它首先會調用使用者的 onInit 和 onStart 方法。使用者會做一些初始化工作,執行他們的程式,然後返回。接著建立一個 channel 與作業系統進行互動。signal.Notify 綁定了 channel 用來接收作業系統的各類事件。代碼接著會執行一個無限迴圈直到作業系統有事件通知程式。代碼會尋找關閉事件。一旦接受到了一個關閉事件,使用者的 onStop 方法就會被調用,然後 Run 方法返回並終止程式。## 服務管理員服務管理員提供了所有的樣板代碼,因此任何程式都能很輕鬆地實現服務。下面的代碼展示了 Config 的 Run 方法:```gofunc (config *Config) Run() {var err errorconfig.Service, err = NewService(config)if err != nil {fmt.Printf("%s unable to start: %s", config.DisplayName, err)return}// Perform a command and then returnif len(os.Args) > 1 {verb := os.Args[1]switch verb {case "install":if err := service.Install(config); err != nil {fmt.Println("Failed to install:", err)return}fmt.Printf("Service \"%s\" installed.\n", config.DisplayName)returncase "remove":if err := service.Remove(); err != nil {fmt.Println("Failed to remove:", err)return}fmt.Printf("Service \"%s\" removed.\n", config.DisplayName)returncase "debug":config.Start(config)fmt.Println("Starting Up In Debug Mode")reader := bufio.NewReader(os.Stdin)reader.ReadString('\n')fmt.Println("Shutting Down")config.Stop(config)returncase "start":if err := service.Start(); err != nil {fmt.Println("Failed to start:", err)return}fmt.Printf("Service \"%s\" started.\n", config.DisplayName)returncase "stop":if err := service.Stop(); err != nil {fmt.Println("Failed to stop:", err)return }fmt.Printf("Service \"%s\" stopped.\n", config.DisplayName)returndefault:fmt.Printf("Options for \"%s\": (install | remove | debug | start | stop)\n", os.Args[0])return}}// Run the serviceservice.Run(config)}```Run 方法一開始通過提供的配置建立了 service 對象。接著查詢傳入的命令列參數。如果參數是一個命令,則相應的命令便會執行,接著程式終止。如果命令是 debug,程式就會以類似服務的方式啟動除非它沒有被作業系統鉤入。點擊 `<enter>` 鍵可以結束程式。如果沒有傳入任何命令列參數,代碼就會通過調用 service.Run 方法嘗試以守護進程的方式啟動。## 服務實現下面的代碼是一個使用 service 架構的例子:```gopackage mainimport ("fmt""path/filepath""github.com/goinggo/service/v1")func main() {// Capture the working directoryworkingDirectory, _ := filepath.Abs("")// Create a config object to start the serviceconfig := service.Config{ExecutableName: "MyService",WorkingDirectory: workingDirectory,Name: "MyService",DisplayName: "My Service",LongDescription: "My Service provides support for…",LogLocation: _Straps.Strap("baseFilePath"),Init: InitService,Start: StartService,Stop: StopService,}// Run any command line options or start the serviceconfig.Run()}func InitService() {fmt.Println("Service Inited")}func StartService() {fmt.Println("Service Started")}func StopService() {fmt.Println("Service Stopped")}```Init,Start 和 Stop 方法執行完必須立即返回 config.Run 方法。My Code已經在 Mac OS 上測試過了。Linux 下的代碼除了建立和安裝的指令碼以外都是一樣的。當然 Start 和 Stop 方法的實現使用了不同的程式。不久以後我會測試一部分 Linux 下的代碼。至於 Windows,則需要重構一下代碼,我就不再構建了。如果你計劃使用 Windows,可以從 Daniel 的代碼開始自行編寫。一旦代碼構建完成,就可以開啟終端並執行不同的命令。```./MyService debug./MyService install./MyService start./MyService stop```和往常一樣,我希望這份代碼能協助你建立和運行自己的服務。
via: https://www.ardanlabs.com/blog/2013/06/running-go-programs-as-background.html
作者:William Kennedy 譯者:alfred-zhong 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1265 次點擊 ∙ 1 贊