C#编写windows服务
前言
1 目标方案
亲戚小孩经常玩游戏,总是玩到很晚,所以目标是不让小孩玩游戏,考虑了一些方案,都感觉不行,比如开机自动关机,禁用网卡,修改 host 文件,修改 iP 和 DNS。
开机自动关机这个是最简单的,只需要一个关机脚本就行了,再加入开机自启项就行了,但是这种方式就是电脑都不给用了,家长不会进PE的话很难修复,要用的时候就很着急,虽然对普通家长来说现在也没那么需要电脑。
剩下的都是一些关于网络控制的,主要是我不会写,特别是针对游戏服务器的地址,这些都不明确,我觉得写不了。还有就是完全不让上网的禁用网关,修改DNS等不太人性化,而且这种都比较好修复,不让上网还是太过分
2 确定方案
所以目标变成了小孩能上网,但是不能玩游戏,完全不能玩游戏其实也不太好办到,毕竟没指定是哪些游戏,市面上游戏那么多我根本控制不过来,扫描全盘游戏也要先知道这目录是游戏才行,或许有方案可以做到,但我不知道,总之我是先做一部分出来,
方案:默认写了一些进程名然后后台每隔5分钟杀一次进程。
3 实现方案
搜了半天windows后台程序怎么写,Java总有控制台不太好,我要的是后台运行,然后就看了计划任务,但是这必须手动创建,用程序创建文档有点多,静不下心来看。
最后选择了windows服务,这种方式是最好的,后台程序开机自启很方便。
但是用什么实现也不好选择,一开始我是用Java写了一个 while 死循环每隔5秒钟去杀掉记事本程序,这只是测试。写完后感觉差不多了,然后百度 Java 程序怎么封装成 windows 服务,弄了好久都不行,使用的 java windows wrapper
来封装,但是我在虚拟机上测试的时候根本无法后台运行,运行一次就终止了,还有的就是一直安装不了服务,或者1067 进程意外终止,应该是 while 循环的问题,没搞定我就放弃了这种,总之就是心累,没想到连这么个简单的程序都写不好。
接着就用 C++ 来试试看,写完编译完也是服务安装后启动不了,或者只执行一次后停止了,还是和 Java 一样的问题,这些都没搞定我就放弃了。
然后就是百度搜到用C#写很容易,于是就试着写了一下,没想到可以了。
虽然 C# 的语法我不太知道,但是面向百度编程嘛,边写边测试就行了。
4 开发环境
Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.5.3
.Net Framework 4.7.2
5 测试环境
VMware 17 Pro 17.0.1 build-21139696
VMware Workstation 17 Pro - 辰羿'S_Blog 可以从这位大佬的网站上下载,这个虚拟机安装windows的步骤网上就很多了,如果嫌麻烦的话也可以在本地机器上安装服务,主要是我之前测试自动关机的时候用windows比较方便,随时都可以改。
Windows 11 22H2
系统下载就是搜msdn下载就行了。需要注意的是我之前安装过 VMware 16,一直装不了windows 10最新版和windows11,搞了半天才知道是VMware版本低了
实现步骤
1 项目初始化以及部分代码
基本上是跟着 基于C#实现Windows服务 来做的。创建项目的难点在于取名字,对于自己项目,取个名字我觉得太难了,跟着教程取 TestService 不太好,当然这些都能改,但是改的地方很多,改漏了也不好,想了一会,就叫 kspService (Kill Specify Process Service) 终止指定进程服务
跟着这些教程,也只是创建了windows服务,没有具体怎么实现功能的,只说了启动和停止会执行的函数,没说运行时间隔一段时间执行代码怎么做,这块百度了很久才知道怎么做。
1.1 创建新项目
1.2 配置新项目
1.3 目录结构
为了和项目名统一,将默认的 Service1 类修改成 KspService
1.4 添加安装程序
双击KspService.cs,在 KspService 设计界面右键,添加安装程序
添加完安装程序后,目录会多出一个 ProjectInstaller.cs 文件,还会出现这个文件的设计节目,界面上有两个组件,分别是 serviceProcessInstaller 和 serviceInstaller,通过右键组件然后点击属性来修改服务配置
1.5 修改 serviceProcessInstaller
可以通过操作服务运行于其中的进程的 Account 属性来设置安全性上下文。 此属性允许将服务设置为以下四种帐户类型之一:
User
,该帐户会导致系统在安装服务时提示输入有效的用户名和密码,并在网络上单个用户指定的帐户的上下文中运行;LocalService
,该帐户在用作本地计算机上的非特权用户的帐户的上下文中运行,并向任意远程服务器提供匿名凭据;LocalSystem
,该帐户在提供广泛本地权限的帐户的上下文中运行,并向任意远程服务器提供计算机凭据;NetworkService
,该帐户在用作本地计算机上的非特权用户的帐户的上下文中运行,并向任意远程服务器提供计算机凭据。
我选择修改为 LocalSystem
如何:为服务指定安全上下文 - .NET Framework | Microsoft Learn
1.6 修改 serviceInstaller
StartType | 说明 |
---|---|
Manual | 该服务必须在安装后手动启动 |
Automatic | 只要重启计算机,服务就将自行启动。 |
Disabled | 服务无法启动。 |
设置自动启动就行
填写描述信息和服务显示信息,DisplayName是从管理里面看服务的名称
其他更具体的说明可以参考微软文档
如何:将安装程序添加到服务应用程序 - .NET Framework | Microsoft Learn
1.7 查看默认服务功能
在 KspService.cs 设计界面右键选择查看代码,代码如下
很明显,这里面只有启动和停止的代码实现
2 编写主要功能
主要功能是每隔一段时间就杀掉游戏进程,这需要哪些函数呢
写入日志文件,简单起见不用什么框架,直接写入文件就行了
杀进程函数,使用百度搜到的杀进程函数
读取文件获取进程名称
暂停,继续功能,默认的服务有启动停止,增加一下暂停和继续来重新读取文件获取进程名称
循环任务,使用 System.Timers.Timer
来每隔一段时间执行一次杀进程函数
2.1 定义用到的参数
/* 使用了using System.Timers; */
private static Timer timer = new Timer(300000);
/* 日志目录,当前目录下的log目录 */
private static readonly string logPath = AppDomain.CurrentDomain.BaseDirectory + "//log";
/* 需要终止进程的进程字文件 */
private static readonly string blackListFile = AppDomain.CurrentDomain.BaseDirectory + "blacklist.txt";
/* 文件里的进程名集合 */
private static HashSet<string> blackListFileSet = new HashSet<string>();
/* 默认提供的进程名集合 */
private static HashSet<string> blackListInnerSet = new HashSet<string>
{
"steam",
"Steam",
"WeGame",
"GTA5",
"Warframe.x64",
"League of Legends",
"YuanShen",
"r5Apex",
"r5apex",
"dota",
"dota2",
"csgo"
};
2.2 写入日志
private void WriteLog(string logContent)
{
if (!Directory.Exists(logPath))
{
Directory.CreateDirectory(logPath);
}
using (FileStream stream = new FileStream(logPath + $"\\log_{DateTime.Now.ToString("yyyyMMdd")}.log", FileMode.Append))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {logContent}");
}
}
}
2.3 杀进程
p.ProcessName.Equals(processName)
采用Equals是为了准确,使用Contains的话,关闭主程序后,当前所有进程快照中还存在包含关键字的进程,但是因为关闭关键字主程序后那些包含关键字的进程已经终止掉了,再终止就会抛异常。
private void KillProcess(string processName)
{
foreach (Process p in Process.GetProcesses())
{
//采用Equals是为了准确, 使用Contains的话有些进程关闭会附带关闭其他带关键字的程序
if (p.ProcessName.Equals(processName))
{
try
{
p.Kill();
p.WaitForExit();
WriteLog($"[{p.ProcessName}] process killed");
}
catch (Win32Exception e)
{
WriteLog(e.Message.ToString());
}
catch (InvalidOperationException e)
{
WriteLog(e.Message.ToString());
}
}
}
}
2.4 读取文件获取进程名称
private void ReadBlackListFile()
{
if (File.Exists(blackListFile))
{
WriteLog($"read {blackListFile}");
using (StreamReader reader = new StreamReader(blackListFile))
{
while (reader.Peek() != -1)
{
string prcessName = reader.ReadLine();
blackListFileSet.Add(prcessName);
WriteLog($"add [{prcessName}] to blacklist");
}
}
}
else
{
WriteLog($"[{blackListFile}] not found");
}
}
2.5 重写暂停继续
构造函数中设置属性 CanPauseAndContinue
为 true
public KspService()
{
InitializeComponent();
this.CanPauseAndContinue = true;
}
然后重写 OnContinue,OnPause,OnShutdown 函数
2.6 循环执行任务
先写一个支持 Timer 执行的函数,主要功能是循环两个集合,终止里面的关键字程序
不写成一个集合是因为不希望完全由文件配置来管理,而且暂停继续需要重写读取文件的内容,到时会清空集合,但是默认的不能清空,因此分开使用两个集合
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
foreach (string processName in blackListInnerSet)
{
KillProcess(processName);
}
foreach (string processName in blackListFileSet)
{
KillProcess(processName);
}
}
2.7 KspService.cs 全量代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace kspService
{
public partial class KspService : ServiceBase
{
/* 使用了using System.Timers; 还可以将间隔设置为5000(5秒)在测试环境测试 */
private static Timer timer = new Timer(5000);
/* 日志目录,当前目录下的log目录 */
private static readonly string logPath = AppDomain.CurrentDomain.BaseDirectory + "//log";
/* 需要终止进程的进程字文件 */
private static readonly string blackListFile = AppDomain.CurrentDomain.BaseDirectory + "blacklist.txt";
/* 文件里的进程名集合 */
private static HashSet<string> blackListFileSet = new HashSet<string>();
/* 默认提供的进程名集合 */
private static HashSet<string> blackListInnerSet = new HashSet<string>
{
"steam",
"Steam",
"WeGame",
"GTA5",
"Warframe.x64",
"League of Legends",
"YuanShen",
"r5Apex",
"r5apex",
"dota",
"dota2",
"csgo"
};
public KspService()
{
InitializeComponent();
this.CanPauseAndContinue = true;
}
protected override void OnStart(string[] args)
{
WriteLog("KillSpecifyProcessService OnStart! ");
ReadBlackListFile();
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
protected override void OnStop()
{
timer.Stop();
WriteLog("KillSpecifyProcessService OnStop! ");
}
protected override void OnContinue()
{
ReadBlackListFile();
timer.Start();
WriteLog("KillSpecifyProcessService OnContinue! ");
}
protected override void OnPause()
{
blackListFileSet.Clear();
timer.Stop();
WriteLog("KillSpecifyProcessService OnPause !");
}
protected override void OnShutdown()
{
timer.Stop();
WriteLog("KillSpecifyProcessService OnShutdown !");
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
foreach (string processName in blackListInnerSet)
{
KillProcess(processName);
}
foreach (string processName in blackListFileSet)
{
KillProcess(processName);
}
}
private void ReadBlackListFile()
{
if (File.Exists(blackListFile))
{
WriteLog($"Read {blackListFile}");
using (StreamReader reader = new StreamReader(blackListFile))
{
while (reader.Peek() != -1)
{
string prcessName = reader.ReadLine();
blackListFileSet.Add(prcessName);
WriteLog($"Add [{prcessName}] to the blackListFileSet");
}
}
}
else
{
WriteLog($"[{blackListFile}] Not Found! ");
}
}
private void WriteLog(string logContent)
{
if (!Directory.Exists(logPath))
{
Directory.CreateDirectory(logPath);
}
using (FileStream stream = new FileStream(logPath + $"\\log_{DateTime.Now.ToString("yyyyMMdd")}.log", FileMode.Append))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {logContent}");
}
}
}
private void KillProcess(string processName)
{
foreach (Process p in Process.GetProcesses())
{
/* 采用Equals是为了准确 */
/* 使用Contains的话,关闭主程序后,当前所有进程快照中还存在包含关键字的进程 */
/* 但是因为关闭关键字主程序后那些包含关键字的进程已经终止掉了,再终止就会抛异常 */
if (p.ProcessName.Equals(processName))
{
try
{
p.Kill();
p.WaitForExit();
WriteLog($"[{p.ProcessName}] Process Killed");
}
catch (Win32Exception e)
{
WriteLog(e.Message.ToString());
}
catch (InvalidOperationException e)
{
WriteLog(e.Message.ToString());
}
}
}
}
}
}
执行脚本
1 创建并启动服务
主要是将程序复制到公共目录,然后通过 InstallUtil.exe 创建服务
@echo off
chcp 65001
if not exist "C:\Users\Public\Documents\StudyService\backup" (
md C:\Users\Public\Documents\StudyService\backup
)
if exist "C:\Users\Public\Documents\StudyService\kspService.exe" (
copy /y C:\Users\Public\Documents\StudyService\kspService.exe C:\Users\Public\Documents\StudyService\backup
del C:\Users\Public\Documents\StudyService\kspService.exe
copy %~dp0\kspService.exe C:\Users\Public\Documents\StudyService
)else (
copy %~dp0\kspService.exe C:\Users\Public\Documents\StudyService
)
if not exist "C:\Users\Public\Documents\StudyService\blacklist.txt" (
copy %~dp0\blacklist.txt C:\Users\Public\Documents\StudyService\blacklist.txt
)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe -i C:\Users\Public\Documents\StudyService\kspService.exe
echo 创建服务成功!
sc start kspService
echo 启动服务成功!
pause
2 更新服务 暂停与继续
@echo off
sc pause kspService
sc continue kspService
3 启动服务
@echo off
sc start kspService
4 删除服务
@echo off
sc stop kspService
C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe -u C:\Users\Public\Documents\StudyService\kspService.exe
if exist "C:\Users\Public\Documents\StudyService\kspService.exe" (
copy /y C:\Users\Public\Documents\StudyService\kspService.exe C:\Users\Public\Documents\StudyService\backup
del C:\Users\Public\Documents\StudyService\kspService.exe
)
5 停止服务
@echo off
sc stop kspService
6 重启服务
@echo off
sc stop kspService
sc start kspService
测试
1 Visual Studio 右键项目生成 .exe 文件
2 我的exe文件在在该路径下
D:\admin\dev\source\repos\kspService\kspService\bin\Debug
3 将生成的程序和脚本复制到虚拟机任意目录下
复制到虚拟机需要先安装 VMware Tools 并重启windows虚拟机系统
复制到任意目录
4 右键 创建服务.bat 使用管理员身份运行
如图表示创建成功
5 检查服务是否运行
打开任务管理器选择详细信息发现服务已经运行了,还可以查看计算机管理服务选项,查看服务是否启动
管理里面查看服务名显示
6 在安装目录下修改 blacklist.txt
修改blacklist.txt,是为了测试暂停继续是否有效,而且也是为了以后增加需要关闭的进程不用重新生成程序
打开我指定的安装目录
C:\Users\Public\Documents\StudyService
编辑 blacklist.txt,增加记事本程序名称,在windows10下是 notepad
在windows11下是 Notepad
,区别就是win11的记事本程序N是大写的
7 操作演示
结尾
写完了终于可以给小孩装上了吧,通过blacklist.txt写一些游戏的进程名就可以后台自动杀进程,再修改定时时间为5分钟 (3000000) 重新生成程序再加上脚本就可以用了。
但是没想到和小孩家长沟通的时候,她说她都碰不了电脑,也不知道电脑密码 ???这真是我没想到的,无语了,你这样搞我怎么控制呢,你都操作不了电脑还想控制小孩玩电脑游戏?这事基本上就算是失败了,所以以后有这种事的话就可以直接说不用做了,真的是既要又要,这种家长既想控制小孩不玩游戏又想不要和小孩有矛盾,竟然连电脑都碰不了,这放纵是你自己作的,什么程序都没用。
虽然搞了这些做了无用功,但是还算学到点东西,这个程序我本身应该不会用,但是以后电脑上有什么自动化的操作也可以照着步骤写
但是这种编写的方式还是太简单了,很多东西都没搞懂,如果需要更复杂的功能可能就不好写了,而且占用内存也很大,这么一个小软件就占用10M多,这种想优化也完全没头绪