轻量级,即是服务部署器,又是服务本体,免命令行,免bat.直接部署和调试代码的东西,也就长这样了.
看这一篇文章应该可以把你构建和调试windows服务这些事儿玩儿转.
一.创建服务
1.可以打开VS,新建一个空的项目或者解决方案,在解决方案中添加服务
2.也可以直接创建服务项目
创建好服务以后,不需要像其他文章说的那样创建什么服务安装器,因为不需要使用命令行手动安装服务,所以那个东西不需要.我们会在代码中实现.
创建一个正式服务用的代码逻辑类
不建议直接在服务中 也就是 WindowsService1.cs(类似名字)的OnStart()函数内直接书写过多的代码,不方便维护,尤其是你的服务代码本身是从已经写好的项目中摘过来的,只要在OnStart函数中初始化和调用目标代码即可.这样比较好维护.
比如你的逻辑文件跟我的一样叫LogServer.cs,name整个服务文件(WindowsService1.cs,我命名为LogServerService.cs)的内容应该是这样的:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.Text;
namespace LogServerService
{
partial class LogServerService : ServiceBase
{
LogServer server;
public LogServerService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
server = new LogServer();
server.Start();
}
protected override void OnStop()
{
server.Stop();
}
}
}
这样,服务就是服务,逻辑就是逻辑.后续如果你项目变动可以把服务里面的代码很方便的摘出来.
二.构建部署器
部署器用来安装,卸载,启动,停止等对服务的操作.
命名为ServiceConfigurator
using System;
using System.Collections;
using System.Configuration.Install;
using System.Reflection;
using System.ServiceProcess;
/// <summary>
/// 服务配置器
/// </summary>
public class ServiceConfigurator
{
#region 检查服务是否存在
/// <summary>
/// 检查服务是否存在
/// </summary>
/// <param name="serviceName"></param>
/// <returns></returns>
public static bool IsServiceExisted(string serviceName)
{
ServiceController[] services = ServiceController.GetServices();
foreach (ServiceController s in services)
{
if (s.ServiceName == serviceName)
{
return true;
}
}
return false;
}
#endregion
#region 启动和停止服务
/// <summary>
/// 启动服务
/// </summary>
/// <param name="serviceName">服务名称</param>
/// <param name="checkStartResultTimes">服务启动命令执行后,执行多少次检查服务启动状态</param>
/// <param name="perTimeCheckStartResultDelay">服务启动命令执行后,每隔多久检测一次服务是否在运行的状态</param>
public static void StartService(string serviceName, int checkStartResultTimes = 100, int perTimeCheckStartResultDelay = 100)
{
if (IsServiceExisted(serviceName))
{
System.ServiceProcess.ServiceController service = new System.ServiceProcess.ServiceController(serviceName);
if (service.Status != System.ServiceProcess.ServiceControllerStatus.Running &&
service.Status != System.ServiceProcess.ServiceControllerStatus.StartPending)
{
service.Start();
for (int i = 0; i < checkStartResultTimes; i++)
{
service.Refresh();
System.Threading.Thread.Sleep(perTimeCheckStartResultDelay);
if (service.Status == System.ServiceProcess.ServiceControllerStatus.Running)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("----服务启动成功----");
Console.ResetColor();
break;
}
if (i == checkStartResultTimes - 1)
{
throw new Exception("启动服务发生错误,服务名:" + serviceName);
}
}
}
}
}
/// <summary>
/// 停止服务
/// </summary>
/// <param name="serviceName">服务名称</param>
public static void StopService(string serviceName)
{
if (IsServiceExisted(serviceName))
{
System.ServiceProcess.ServiceController service = new System.ServiceProcess.ServiceController(serviceName);
if (service.Status != System.ServiceProcess.ServiceControllerStatus.Stopped &&
service.Status != System.ServiceProcess.ServiceControllerStatus.StopPending)
{
service.Stop();
for (int i = 0; i < 60; i++)
{
service.Refresh();
System.Threading.Thread.Sleep(1000);
if (service.Status == System.ServiceProcess.ServiceControllerStatus.Stopped)
{
Console.WriteLine("服务已停止");
break;
}
if (i == 59)
{
throw new Exception("停止服务发生错误,服务名:" + serviceName);
}
}
}
}
}
#endregion
#region 获取服务状态
/// <summary>
/// 获取服务状态
/// </summary>
/// <param name="serviceName">服务名称</param>
/// <returns></returns>
public static ServiceControllerStatus GetServiceStatus(string serviceName)
{
System.ServiceProcess.ServiceController service = new System.ServiceProcess.ServiceController(serviceName);
return service.Status;
}
#endregion
#region 安装和卸载服务
/// <summary>
/// 配置服务(安装或者卸载)
/// </summary>
/// <param name="serviceName">服务名称</param>
/// <param name="serviceSrcDir">要把服务的应用程序安装到哪个文件夹下面,以后启动服务的时候从那里直接启动</param>
/// <param name="displayName">服务在服务控制台中显示的名字,可以直接使用服务名称也可以指定其他的名称如中文名</param>
/// <param name="desc">服务详情的描述</param>
public static void InstallService(string serviceName, string serviceSrcDir, string displayName, string desc)
{
TransactedInstaller installer = BuidInstaller(serviceName, displayName, desc);
#region 复制文件到目标位置然后再进行安装
string debuggingFilePath = Assembly.GetEntryAssembly().Location;
string debuggingFileName = System.IO.Path.GetFileName(debuggingFilePath);
string debuggingFileDir = System.IO.Path.GetDirectoryName(debuggingFilePath);
if (serviceSrcDir.EndsWith("\\"))
{
serviceSrcDir = System.IO.Path.GetDirectoryName(serviceSrcDir);
}
#region 没有目标文件夹自动创建
if (System.IO.Directory.Exists(serviceSrcDir) == false)
{
System.IO.Directory.CreateDirectory(serviceSrcDir);
}
#endregion
string installService2FilePath = string.Format("{0}\\{1}", serviceSrcDir, debuggingFileName);
System.IO.File.Copy(debuggingFileName, installService2FilePath, true);
#endregion
installer.Context.Parameters["assemblypath"] = installService2FilePath;
//ti.Context.Parameters["assemblypath"] = Assembly.GetEntryAssembly().Location;
installer.Install(new Hashtable());
Console.WriteLine("安装命令启动完成");
}
/// <summary>
/// 卸载服务
/// </summary>
/// <param name="serviceName">服务名称</param>
public static void UnInstallService(string serviceName)
{
TransactedInstaller installer = BuidInstaller(serviceName);
installer.Uninstall(null);
Console.WriteLine("卸载命令启动完成");
}
#endregion
#region 安装和卸载服务时,使用的安装器对象构建
/// <summary>
/// 构建安装器,在卸载时,直接指定第一参数即可
/// </summary>
/// <param name="serviceName">服务名称</param>
/// <param name="displayName">服务在服务管理器中的显示名称</param>
/// <param name="desc">服务相关描述</param>
static TransactedInstaller BuidInstaller(string serviceName, string displayName = null, string desc = null)
{
TransactedInstaller ti = new TransactedInstaller();
ti.Installers.Add(new ServiceProcessInstaller
{
Account = ServiceAccount.LocalSystem
});
ti.Installers.Add(new ServiceInstaller
{
DisplayName = (displayName == null ? serviceName : displayName),
ServiceName = serviceName,
Description = desc == null ? "安装服务时未指定服务描述" : desc,
//ServicesDependedOn = new string[] { "Netlogon" },//此服务依赖的其他服务
StartType = ServiceStartMode.Automatic//运行方式
});
ti.Context = new InstallContext();
return ti;
}
#endregion
}
服务和部署器都创建完成,构建一个又能自动化配置服务,又能作为服务被windows启用的Program.cs文件
三.构建部署器操控器&服务本体功能于一体的Program.Main
using System;
using System.Diagnostics;
using System.ServiceProcess;
namespace LogServerService
{
static class Program
{
#region 全局变量,根据自己的使用情况直接修改该包围块中的内容即可
/// <summary>
/// 服务的名称.
/// </summary>
public static string ServiceName = "LogServerService";
/// <summary>
/// 服务显示名称
/// </summary>
public static string ServiceDisplayName = "LogServerService";
/// <summary>
/// 服务描述
/// </summary>
public static string ServiceDesc = "潮咖医疗付药机看门狗程序,用以检测付药机程序的状态及自动启动付药机程序";
/// <summary>
/// 服务的exe要安装到哪个文件夹下,生成了服务以后直接把文件复制到目标位置,然后安装的时候使用这个位置.
/// </summary>
public static string ServiceInstallPath = "D:\\付药机看门狗服务";
/// <summary>
/// 用于运行console模式的时候,使用的服务相关处理情况日志目录,为保证日志文件不会过大,通常只在服务启动错误的时候使用.
/// </summary>
public static string ConsoleTestLogFilePath = "c:\\windows服务部署器日志.log";
#endregion
/// <summary>
/// 应用程序的主入口点。
/// </summary>
static void Main(string[] args)
{
#region 不参数启动exe文件的时候就是启动服务的进程
if (args == null || args.Length == 0)
{
try
{
ServiceBase[] serviceToRun = new ServiceBase[] { new LogServerService() };
ServiceBase.Run(serviceToRun);
}
catch (Exception ex)
{
System.IO.File.AppendAllText(ConsoleTestLogFilePath, "\r\n服务启动失败,启动时间:" + DateTime.Now.ToString() + "\r\n错误信息:" + ex.Message);
}
}
#endregion
#region 带参数启动exe的时候就是运行安装部署器,在项目的属性->调试->启动选项->命令行参数 内 添加任意参数即可.可以根据具体的情况确认是否严格校验参数或指定不同的参数对应的功能.
else
{
//开始标记的位置,可供goto使用
StartLocation:
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("********************************************************");
Console.ResetColor();
Console.WriteLine("当前时间:{0}", DateTime.Now);
Console.WriteLine("1:删除服务(如存在)+安装服务+启动服务");
Console.WriteLine("2:安装服务");
Console.WriteLine("3:卸载服务");
Console.WriteLine("4:服务状态检查");
Console.WriteLine("5:启动服务");
Console.WriteLine("6:停止服务");
Console.WriteLine("7:删除服务(使用sc delete 服务名)");
Console.WriteLine("8:调试服务逻辑代码");
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("********************************************************");
Console.ResetColor();
ConsoleKey key = Console.ReadKey().Key;
#region 按键1自动部署服务 如果存在服务,卸载,然后重新安装,安装后启动.
if (key == ConsoleKey.NumPad1 || key == ConsoleKey.D1)
{
if (ServiceConfigurator.IsServiceExisted(ServiceName))
{
ServiceConfigurator.UnInstallService(ServiceName);
}
if (!ServiceConfigurator.IsServiceExisted(ServiceName))
{
ServiceConfigurator.InstallService(ServiceName, ServiceInstallPath, ServiceDisplayName, ServiceDesc);
}
ServiceConfigurator.StartService(ServiceName);
goto StartLocation;
}
#endregion
#region 按键2的安装服务
else if (key == ConsoleKey.NumPad2 || key == ConsoleKey.D2)
{
if (!ServiceConfigurator.IsServiceExisted(ServiceName))
{
ServiceConfigurator.InstallService(ServiceName, ServiceInstallPath, ServiceDisplayName, ServiceDesc);
}
else
{
Console.WriteLine("\n服务已存在......");
}
goto StartLocation;
}
#endregion
#region 按键3的卸载服务
else if (key == ConsoleKey.NumPad3 || key == ConsoleKey.D3)
{
if (ServiceConfigurator.IsServiceExisted(ServiceName))
{
ServiceConfigurator.UnInstallService(ServiceName);
}
else
{
Console.WriteLine("\n服务不存在......");
}
goto StartLocation;
}
#endregion
#region 按键4的查看服务状态
else if (key == ConsoleKey.NumPad4 || key == ConsoleKey.D4)
{
if (!ServiceConfigurator.IsServiceExisted(ServiceName))
{
Console.WriteLine("\n服务不存在......");
}
else
{
Console.WriteLine("\n服务状态:" + ServiceConfigurator.GetServiceStatus(ServiceName).ToString());
}
goto StartLocation;
}
#endregion
#region 按键5的启动服务
else if (key == ConsoleKey.NumPad5 || key == ConsoleKey.D5)
{
ServiceConfigurator.StartService(ServiceName);
Console.WriteLine("执行启动后的服务状态:" + ServiceConfigurator.GetServiceStatus(ServiceName).ToString());
goto StartLocation;
}
#endregion
#region 按键6的停止服务
else if (key == ConsoleKey.NumPad6 || key == ConsoleKey.D6)
{
ServiceConfigurator.StopService(ServiceName);
Console.WriteLine("执行停止后的服务状态:" + ServiceConfigurator.GetServiceStatus(ServiceName).ToString());
goto StartLocation;
}
#endregion
#region 按键7的删除服务使用sc
else if (key == ConsoleKey.NumPad7 || key == ConsoleKey.D7)
{
Console.WriteLine("正在使用sc命令删除服务......");
Process p = new Process();
p.StartInfo.FileName = @"C:\WINDOWS\system32\cmd.exe ";
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.CreateNoWindow = true;
p.Start();
p.StandardInput.WriteLine(string.Format("net stop {0}", ServiceName));
p.StandardInput.WriteLine(string.Format("sc delete {0}", ServiceName));
p.StandardInput.WriteLine("exit");
p.WaitForExit();
p.Close();
p.Dispose();
//Process pr = new Process();
//pr.StartInfo.FileName = "sc";
//pr.StartInfo.Arguments = string.Format(" delete {0}", ServiceName);
//pr.Start();
if (ServiceConfigurator.IsServiceExisted(ServiceName) == false)
{
Console.WriteLine("执行sc删除服务命令完成");
}
else
{
Console.WriteLine("执行sc删除服务命令失败");
}
goto StartLocation;
}
#endregion
#region 按键8的调试服务逻辑代码
else if (key == ConsoleKey.NumPad8 || key == ConsoleKey.D8)
{
LogServer server = new LogServer();
server.Start();
goto StartLocation;
}
#endregion
#region 其他无效的按键输入
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("无效的输入");
Console.ResetColor();
goto StartLocation;
}
#endregion
}
#endregion
}
}
}
主要就是这些,剩下的就是编写你的LogServer.cs 也就是正式逻辑运行的代码了.
四.构建可以用来调试的逻辑代码
我的项目是用来自动检测计算机上的某个程序的运行状态,检测到他不在线的话,就启动该程序,方案可以有很多,可以用管道(匿名和命名管道都可以),也可以用读写公共文件的方式,读写数据库的方式,读写公共内存的方式,TCP的方式,WS的方式,HTTP的方式等等,总之是让两个程序可以互相通讯,检测到被检测的程序没有心跳了,关掉现有的卡死的程序(如果存在),重新启动被检测的程序.
为了保证服务可以被调试,构建的逻辑代码中,应该检测是否被调试器所附加到了进程,如果没有被附加到进程,正常的逻辑已经开始跑了,你再去附加到进程的话,是没有办法保证代码能被调试的.所以,要在逻辑被启动了以后,检测是否被调试器附加,检测代码是在一个持续运行的循环中:
void bw_DoWork(object sender, DoWorkEventArgs e)
{
Console.ForegroundColor = ConsoleColor.Blue;
string startedMsg =string.Format("\r\n进入到服务bw_DoWork当前时间:{0}\r\n调试模式?:{1}\r\n配置文件路径:{2}\r\n日志文件路径:{3}\r\n",
DateTime.Now,setting.Debugging, this.settingFilePath, this.setting.LogFilePath);
Console.WriteLine(startedMsg);
Console.ResetColor();
System.IO.File.AppendAllText(setting.LogFilePath, startedMsg);
while (bw.CancellationPending == false)
{
//2021年12月10日10:41:12 调试的时候 只有当调试器介入了的时候,才会正常运行逻辑代码.非调试正常部署服务的时候这个要去掉.
if (setting.Debugging && System.Diagnostics.Debugger.IsAttached == false)
{
System.Threading.Thread.Sleep(17);
continue;
}
#region 正常代码
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("此消息来自服务执行逻辑{0}", DateTime.Now);
Console.ResetColor();
System.Threading.Thread.Sleep(5000);
System.IO.File.AppendAllText(setting.LogFilePath, "进入到服务正常代码" + DateTime.Now.ToString() + "\r\n");
continue;
#endregion
}
//throw new NotImplementedException();
}
这里我使用到了Setting中的Debugging控制是否在调试,如果不是在调试的时候,服务正常启动,还仍然等待调试器介入,那这个服务的逻辑代码将永不会被执行.
bw是一个backgroundworker,bw_dowork是他的工作运行函数.
详细的逻辑可以研究一下我的代码,从Program.Main函数开始一步一步调试就知道了.
五.每次更新代码后的调试或重新安装服务
服务一旦被安装以后,代码做了修改,需要重新的安装或者停止再启动,为了方便,我们启动调试以后,直接让程序进入到部署器,这里在项目的调试模式中加了参数,只要有参数,main函数检测到有参数运行就是调试.如果没有参数,就是正常的服务被windows加载和启动.设置如下:
右键项目,属性
目前我的代码中 只要有参数就可以 随便指定 你可以根据你的项目做修改.
启动调试以后,出来的窗口如下:
由于构建服务到安装服务都是不可跟踪调试的,所以单独加了一个调试代码的功能,就是在Program的main中,检测到按键8就new一个LogServer的对象然后执行Start()模拟服务被启动的时候的代码,这样每次改了代码进行调试的时候直接按一下8就会走服务的正式代码.这也是把服务文件WindowsService1.cs和主要服务代码LogServer单独摘出来的原因.
六.服务安装并运行起来以后的调试
windows服务项目在开发的时候不像winform或者控制台应用程序那样可以直接调试,需要附加到进程调试(也可以使用上面提到的按8键进入主体代码更方便调试).
附加到进程的方法是,启动了上面的黑框框也就是vs调试器运行起来,你更新了服务之类的完事儿了以后,不需要关闭当前框框,也可以是在vs空闲的时候 点击菜单栏的 调试->附加到进程->勾选"显示所有用户的进程",找到服务名称.exe 我这个是LogServerService.exe ,然后点附加
这时候就会走到你要调试的代码中了.也就是LogServer的do_work函数内的while内的正常代码部分了.也就是说,如果你设定在调试服务,那么服务就会等着你的vs把他附加到调试器
七.补充
由于服务实际上是要我们安装到目标机器,而不是就在vs的debug目录下就当服务程序的实际位置了.所以在安装服务的时候.为大家考虑到了服务的实际执行路径,根据你自己的设置配置一下program中的参数,安装服务的时候会自动把服务的exe程序复制到指定的目录,然后安装服务的时候使用指定目录的文件进行服务安装.就相当于把服务单独摘出来了,复制到目标机 在cmd或者创建快捷方式的方式和 启动那个exe(比如我的是LogServerService.exe)带着参数,就可以进行部署了就想开头的图片那样,安装后会自动启动.
比如cmd这样操作:
LogServerService.exe -xxxx 启动就是部署器,不带参数启动,就是个服务,也可以直接启动就相当于一个控制台应用程序(运行正常服务逻辑内容)
或者新建一个该文件的快捷方式 然后修改属性为:
运行这个快捷方式也可以直接启动部署器部分了
如果exe有依赖一些dll,还需要你自己做一些修改,不然服务启动不了.
八.最后上代码
是希望各位能知其然知其所以然,对基础的东西有一个比较深入和全面的了解的话,再怎么复杂的东西也不是问题,因为世界上任何复杂的东西都是由简单的东西构造起来的.
该项目中的文件如果直接运行,就给你安装了LogServerService的服务,如果相应的想改名之类的,直接改类名称让vs自动修改引用,然后改一些Program.cs中的static的变量就行了.
逻辑的部分直接修改LogServer.cs中的bw_dowork中标记为#region正式代码 #endregion中的相关部分就可以了.
#region 正常代码
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("此消息来自服务执行逻辑{0}", DateTime.Now);
Console.ResetColor();
System.Threading.Thread.Sleep(5000);
System.IO.File.AppendAllText(setting.LogFilePath, "进入到服务正常代码" + DateTime.Now.ToString() + "\r\n");
continue;
#endregion