我如何 运行 来自 Windows 服务的另一个可执行文件

How do I run another executable from a Windows service

除了一些关于 Go 的教程外,我没有任何实际经验。我正在尝试将一个用 Go 编写的项目转换为 windows 服务。

老实说,除了试图找到要阅读的内容之外,我没有尝试过任何其他方法。我找到了一些线程并选择了我认为满足我们所有需求的最佳库

https://github.com/golang/sys

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package main

import (
    "fmt"
    "strings"
    "time"

    "golang.org/x/sys/windows/svc"
    "golang.org/x/sys/windows/svc/debug"
    "golang.org/x/sys/windows/svc/eventlog"
)

var elog debug.Log

type myservice struct{}

func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
    const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
    changes <- svc.Status{State: svc.StartPending}
    fasttick := time.Tick(500 * time.Millisecond)
    slowtick := time.Tick(2 * time.Second)
    tick := fasttick
    changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
    for {
        select {
        case <-tick:
            beep()
            elog.Info(1, "beep")
        case c := <-r:
            switch c.Cmd {
            case svc.Interrogate:
                changes <- c.CurrentStatus
                // Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4
                time.Sleep(100 * time.Millisecond)
                changes <- c.CurrentStatus
            case svc.Stop, svc.Shutdown:
                // golang.org/x/sys/windows/svc.TestExample is verifying this output.
                testOutput := strings.Join(args, "-")
                testOutput += fmt.Sprintf("-%d", c.Context)
                elog.Info(1, testOutput)
                break loop
            case svc.Pause:
                changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
                tick = slowtick
            case svc.Continue:
                changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
                tick = fasttick
            default:
                elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
            }
        }
    }
    changes <- svc.Status{State: svc.StopPending}
    return
}

func runService(name string, isDebug bool) {
    var err error
    if isDebug {
        elog = debug.New(name)
    } else {
        elog, err = eventlog.Open(name)
        if err != nil {
            return
        }
    }
    defer elog.Close()

    elog.Info(1, fmt.Sprintf("starting %s service", name))
    run := svc.Run
    if isDebug {
        run = debug.Run
    }
    err = run(name, &myservice{})
    if err != nil {
        elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
        return
    }
    elog.Info(1, fmt.Sprintf("%s service stopped", name))
}

所以我花了一些时间检查这段代码。对其进行测试以查看其作用。它按预期执行。

我的问题是我们目前有一个接受参数的 Go 程序,并为我们的服务传入服务器。这会在本地主机网页上启动我们的内容。

我相信上面的代码可能与此有关,但我不知道如何使用正确的参数将它从我们的 exe 中分离出来。这是调用 main 的正确位置吗?

很抱歉,如果这含糊不清。我不知道如何使它与我们已经存在的 exe 交互。

如果我知道需要更改什么,我可以修改它。我很感激任何帮助。

好的,现在清楚多了。好吧,理想情况下,您应该从一些关于什么构成 Windows 服务的教程开始——我敢打赌这可能已经为您解决了问题。不过我们还是试试吧。

一些理论

Windows 服务有两个方面:它执行一些有用的任务并与 SCM facility 通信。当您使用 sc 命令或通过控制面板操作服务时,您可以使用该软件代表您与 SCM 对话,然后 SCM 与该服务对话。

SCM 和服务使用的确切协议是低级和复杂的 你使用的 Go 包的目的是向你隐藏这种复杂性 并为这些东西提供一个合理的以 Go 为中心的界面。

您可能会从自己的示例中了解到,您创建的类型的 Execute 方法在很大程度上与 SCM 通信有关:它 运行 是一个无休止的过程for 循环,每次迭代都会在从 r 通道读取数据时休眠,并且该通道会向您的服务传送 SCM 命令。

所以你基本上拥有了所谓的"an SCM command processing loop".

现在回想一下上面的两个方面。您已经拥有其中一个:您的服务与 SCM 交互,因此您需要另一个 - 实际执行有用任务的代码。

事实上,它已经部分存在了:您获取的示例代码创建了一个时间行情,它提供了一个通道,当另一个行情经过时,它会在该通道上传递一个值。 Execute 方法中的 for 循环也从该通道读取,"doing work" 每次发出另一个报价单时。

好的,这对于玩具示例来说还不错,但对于实际工作来说就很蹩脚了。

接近解决方案

所以让我们暂停一下,考虑一下我们的要求。

  1. 我们需要一些代码运行来完成我们的实际任务。
  2. 我们需要现有的命令处理循环才能继续工作。
  3. 我们需要这两段代码同时工作。

在这个玩具示例中,第 3 个点在那里 "for free" 因为时间自动收报机执行等待下一个滴答声的任务,并且与其余代码完全并发。

您的真实代码很可能不会有那么多奢侈,那您怎么办?

在 Go 中,当您需要同时做某事时, 一个明显的答案是 "use a goroutine".

所以第一步是抓住你现有的代码,把它变成一个可调用的函数 然后在进入 for 循环之前在一个单独的 goroutine 中调用它。 这样,您将同时拥有两件 运行。

困难的部分

好的,这并不难。

难的部分是:

  • 如何配置执行任务的代码。
  • 如何使单片机命令处理循环和执行任务的代码通信。

配置

这确实取决于您的 $dayjob 或 $current_project 的政策,但有一些提示:

  • A Windows 服务可能会接收命令行参数——单个 运行 或永久(在其每个 运行 上传递给服务).

    缺点是从 UI/UX 的角度来看,使用它们并不方便。

  • 通常 Windows 用于读取注册表的服务。

  • 如今(在 .NET 及其普及 xml-ity 出现之后)服务倾向于读取配置文件。

  • 大多数时候 OS 环境不适合这项任务。

  • 您可以组合其中的几个场所。

我想我会从一个配置文件开始,但话又说回来,我认为你应该选择阻力最小的路径。

要记住的一件事是,配置的读取和处理最好在服务向 SCM 发出它启动正常的信号之前完成:如果配置无效或无法加载,服务应该广泛记录并发出失败信号,不是运行实际任务处理代码。

命令处理循环与携带代码的任务之间的通信

这是 IMO 最难的部分。

这里可以写一整本书,但现在让我们保持简单。

为了尽可能简单,我将执行以下操作:

  • 考虑一下暂停、停止和关闭大部分是相同的:所有这些信号都必须告诉您的任务处理代码退出,然后等待它实际执行。
  • 考虑 "continue" 信号与启动任务处理函数相同:运行 再次——在一个新的 goroutine 中。
  • 进行单向通信:从控制循环到任务处理代码,而不是相反——这将大大简化服务状态管理。

这样,您可以创建任务处理代码侦听或定期检查的单个通道,当值来自该通道时,代码停止 运行ning,关闭通道并退出。

控制循环,当 SCM 告诉它暂停、停止或关闭时,在该通道上发送任何东西,然后等待它关闭。发生这种情况时,它知道任务处理代码已完成。

在 Go 中,仅用于发信号的通道的范例是具有 struct{} 类型的通道(空 struct)。

如何在任务 运行ning 代码中监视此控制通道的问题是一个开放的问题,在很大程度上取决于它执行的任务的性质。

这里的任何进一步帮助都是背诵 Go 书中关于并发的内容,所以你应该先


还有一个有趣的问题,即如何使控制循环和任务处理循环之间的通信能够弹性应对后者可能出现的处理停顿,但话又说回来,IMO 现在谈这个还为时过早。