是的,我知道,但我仍然想要一个 Windows 服务的 GUI!






4.75/5 (17投票s)
您可能有很多理由希望为您的 Windows 服务创建一个界面。下面是如何做到这一点!
引言
您可能有很多理由希望为您的服务创建一个界面:监控服务活动、运行测试工具、为服务消费者提供临时模拟数据、测试上游接口等。一个用户界面可以为您提供对一个通常不为人知的进程运行情况的有用可见性。
当然,大家都知道 Windows 服务不能有用户界面,是吗?在某些情况下,您可以强制服务进入交互模式,至少在 Vista 之前是这样。请参阅这篇文章,其中介绍了一种可能对您有用的复杂技术。不过,正如作者所说,它“通常不值得付出努力”。如果只需要一个简单的消息框和响应,还可以使用WTSSendMessage。
对于更复杂的界面,您可以创建一个单独的 GUI 控制器应用程序,通过自定义协议通过某个 IPC 通道与服务进行通信。只要您的服务成功达到可以与您的控制器应用程序通信的状态,这就可以工作。附加调试器到正在运行的服务是可能的,但这有局限性。而且,当(如我们将要看到的)一个应用程序就足够时,谁还想维护两个应用程序呢?
因此,让我们退一步考虑一下,我们是否需要服务拥有 GUI,或者服务 *看起来* 拥有 GUI 是否就足够了。如果是这样,本文将向您展示如何实现这一目标并满足以下要求:
1.您只需要创建一个单一的应用程序。
2.GUI 和服务之间不会以任何方式相互干扰,除非您希望如此。
3.服务消费者将不知道 GUI 正在运行(除非您告诉他们)。
4.即使 GUI 正在运行,用于响应服务请求的也是完全相同的代码。
背景
Windows 会为每个登录用户创建一个“会话”。从 Vista 开始,会话 0 保留给服务和非交互式用户应用程序;用户在会话 1 及更高版本中运行。此切换主要是出于安全原因,以防止用户应用程序访问可能具有特权的服务。为了完成隔离,在会话 0 中运行的进程被禁止访问图形硬件。更多详细信息可在此处找到。
解决方案概览
我们将从创建一个标准的 Windows 服务应用程序开始;Visual Studio 使这非常容易。当应用程序运行时,它将像任何其他服务一样调用 ServiceBase.Run(new Service())
。然后,我们将向项目中添加一个 Form
。在启动时检查 System.Environment.UserInteractive
(或者,使用命令行参数以获得更多选项),如果应用程序以交互模式运行,则调用 Application.Run(new Form1())
而不是 ServiceBase.Run()
,然后调用 OnStart()
。为了满足上述要求 #2,首先使用 ServiceController
在 GUI 运行时停止该服务(如果它正在运行)。当 GUI 退出时,使用 ServiceController
在 GUI 启动之前重新启动该服务(如果它之前正在运行)。您将能够从服务模式切换到 GUI 模式,反之亦然,服务消费者很可能永远不会知道。
解决方案,一步一步来
步骤 1:创建服务项目
有很多关于如何创建服务的文章和教程。如果您对此不熟悉,这里有一个链接供您入门。
我们今天的目标是创建一个简单的 Windows 服务,在后续步骤中可以对其进行代码增强。这是我的服务的 VS2015 解决方案资源管理器和 Program.cs
。
步骤 2:让服务做一些事情
在这篇文章中,我的服务将只周期性地向文件写入一行。在实际的服务中,您可能会做一些更有趣的事情,例如监听套接字或消息队列上的请求。为了方便起见,我将 OnStart
和 OnStop
方法放在 Program.cs
中。它们将从 Program.Main
和 ServiceGui1.OnStart/OnStop
调用。
static public class Program
{
static Timer theTimer;
static int lineNumber = 0;
/// (summary)
/// The main entry point for the application.
/// (/summary)
static void Main()
{
ServiceBase.Run(new ServiceGui1());
}
/// (summary)
/// This method receives control when either the service or the GUI starts.
/// For this demo, it will simply start a timer, which will periodically
/// write a line to a file.
/// (/summary)
/// (param name="args")(/param)
static internal void OnStart(string[] args)
{
theTimer = new Timer(tickHandler, null, 1000, 2000);
}
/// (summary)
/// Shut down the timer.
/// (/summary)
static internal void OnStop()
{
theTimer.Change(0, Timeout.Infinite);
theTimer.Dispose();
}
/// (summary)
/// Write a line to the file. For simplicity, assume no race conditions.
/// (/summary)
/// (param name="state")(/param)
static private void tickHandler(object state)
{
string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine);
File.AppendAllText(@"C:\ServiceGuiLog.txt", line);
}
}
步骤 3:向项目中添加一个窗体
窗体可以很简单,也可以很复杂,取决于您。我的 GUI 通常有一个菜单结构,允许我设置操作或测试模式,运行单元或集成测试,查看发生的请求和响应,向服务消费者发送模拟响应,向上游发送模拟请求等等。对于本文,一个简单的公共 ListBox
来查看文件就足够了。
步骤 4:添加服务控制器方法
通常您不希望服务和 GUI 同时运行,例如当它们监听端口进行套接字连接时。如果服务正在运行,ServiceController
将在 GUI 运行时停止服务,然后在 GUI 退出时重新启动服务。(当然,如果您在 GUI 运行时启动服务,将会出现问题,所以不要那样做,但如果您必须这样做,请添加适当的互锁机制。)
/// (summary)
/// If the service is running, stop it.
/// (/summary)
private static void DisableServiceIfRunning()
{
try
{
// ServiceController will throw System.InvalidOperationException if service not found.
ServiceController sc = new ServiceController("ServiceGui1");
sc.Stop();
serviceWasRunning = true;
Thread.Sleep(1000); // wait for service to stop
}
catch (Exception)
{
}
}
/// (summary)
/// If the service was running, start it up again.
/// (/summary)
private static void EnableServiceIfWasRunning()
{
if (serviceWasRunning)
{
try
{
// ServiceController will throw System.InvalidOperationException if service not found.
ServiceController sc = new ServiceController("ServiceGui1");
sc.Start();
}
catch (Exception)
{
}
}
}
步骤 5:集成服务和 GUI 模式
在 Program.cs
中进行以下更改以选择正确的模式。如果涉及到技巧,那就是这个了。
using System.Windows.Forms; static Form1 theForm; /// (summary) /// The main entry point for the application. /// STAThread for the benefit of the GUI; service will ignore it. /// (/summary) [STAThread] static void Main(string[] args) { if (Environment.UserInteractive) { DisableServiceIfRunning(); OnStart(args); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); theForm = new Form1(); Application.Run(theForm); OnStop(); EnableServiceIfWasRunning(); } else { ServiceBase.Run(new ServiceGui1()); } }
如下修改 tickHandler
/// (summary) /// Write a line to the log file. For simplicity, assume no race conditions. /// Modified to also display the line in the GUI if it is running. /// (/summary) /// (param name="state")(/param) static private void tickHandler(object state) { string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine); if (Environment.UserInteractive) { MethodInvoker action = () => { theForm.listBox1.Items.Add(line); theForm.listBox1.TopIndex = theForm.listBox1.Items.Count - 1; }; theForm.BeginInvoke(action); } File.AppendAllText(@"C:\ServiceGuiLog.txt", line); }
您应该能够生成并运行该应用程序,然后看到 GUI 弹出。
步骤 6:安装并启动服务
我包含了一些用于安装和卸载服务的批处理文件。它们使用 v4.0.30319 InstallUtil
;如果您需要使用不同的版本,请修改批处理文件。运行 services.msc
以验证服务已安装。启动它。验证是否正在向文件写入行。
步骤 7:启动 GUI
如果一切正常,您应该会看到行出现在列表框中。验证是否仍在向文件写入行。换句话说,应用程序仍然像服务一样运行。
步骤 8:退出 GUI
服务将被重新启动。验证是否仍在向文件写入行。
轮到你了
就是这样。我希望您觉得这篇文章有趣,甚至可能有用。您可能对当前的 ServiceGui
感到满意,或者您可能已经在考虑一些您想看到的增强功能。请告诉我您做了一些巧妙的修改。
历史
2017 年 1 月 2 日:初始版本