C#模拟键盘鼠标之三:windows服务配合改善模拟流程
距离上一篇文章过去了很久,因为要将模拟键盘鼠标的模块移植到公司项目里面去,在此过程中遇到了不少问题,主要的问题有如下三个:
1、由于Timer对于每个事件都是引发一个新线程,由于模拟是连续性的,这样会引发事件之间的相互干扰。
2、模拟过程中需要有一些业务数据的支持,以及在模拟结束之后需要返回模拟结果
3、模拟过程当中,如果用户操作键盘鼠标怎么办
首先我先分享一下解决以上问题,那我们先从1开始吧。
因为Timer对于每一个定时间隔的事件都是新起一个线程,这种情况下,我们的每一个步骤就没办法变成连续性的,有一些步骤可能会变成异步执行或者在某些步骤执行时间过长的情况下,又会变成颠倒顺序的。从而导致模拟过程失败。此时有的同学可能也会跟我一样,想利用线程加锁来防止模拟的延续性。代码如下:
1 static object lockObj = new object();
2
3 this.timer.Elapsed += (_s, _e) => {
4 lock (lockObj) {
5 //模拟步骤代码
6 }
7 };
利用以上的方法的确可以保持模拟的延续性,然而又会引发另两个问题,1、某个事件运行时间过长(超过间隔时间),会导致同一个事件执行多次;2、当最后一个事件运行时间过长(超过间隔时间),会导致步骤计数超出模拟步骤长度,从而引发异常。鉴于以上的原因,于是我就在想,用什么样的方式可以保持每一个步骤都只运行一次,于是便有了一下的改变,代码如下:
1 static int syncPoint = 0;
2
3 this.timer.Elapsed += (_s, _e) =>{
4 int sync = Interlocked.CompareExchange(ref syncPoint, 1, 0);
5 if (sync == 0){
6 //模拟步骤代码
7 }
8 }
以下是InterLocked的官方解释:
此类的方法可以防止可能在下列情况发生的错误:计划程序在某个线程正在更新可由其他线程访问的变量时切换上下文;或者当两个线程在不同的处理器上并发执行时。
解决了模拟过程的问题之后,接下来就是关于嵌入业务数据的问题了。我使用的方法是在root节点下,加一个引入类命名空间的属性,利用反射 + Call节点 + If节点 + ElseIf节点去解决获取业务数据,以及根据传入的业务数据进行一下业务数据匹配、自动录入的功能。最后的返回模拟结果,是利用Call节点可返回参数的方式以及在解析前,加入回调委托来完成的。Call节点的改造代码如下:
1 class CallStep : AbstractStep
2 {
3 #region 变量
4
5 /// <summary>
6 /// 方法委托
7 /// </summary>
8 Func<object, object[], object> methodInvoke;
9
10 /// <summary>
11 /// 类对象
12 /// </summary>
13 object classObj;
14
15 #endregion
16
17 #region 方法
18
19 /// <summary>
20 /// 执行方法
21 /// </summary>
22 public override void Execute()
23 {
24 var result = this.methodInvoke(this.classObj, new object[] { this.ParentStep.Wnd });
25 if (result != null)
26 {
27 var pairResult = (KeyValuePair<EnumStepState, string>)result;
28 this.State = pairResult.Key;
29 this.Message = pairResult.Value;
30 }
31 }
32
33 /// <summary>
34 /// 反射方法
35 /// </summary>
36 /// <param name="reflector">反射对象</param>
37 public void ReflectorMethod(SimpleReflector reflector)
38 {
39 this.classObj = reflector.GetClassObject();
40 MethodInfo method = reflector.GetMethodInfo(this.Node.Value);
41 this.methodInvoke = new ExpTreeMethod(method).GetMethodInvoke();
42 }
43
44 #endregion
45 }
第3个问题主要是在控制键盘鼠标的消息,我只想到了2个方法去解决这个问题:1、使用全局钩子;2、利用禁用驱动。(下载)
全局钩子的代码已经附上,至于禁用驱动,这个类库是用C++写的,直接引用到项目里面,使用代码如下:
1 //引用API
2 [DllImport("StopKbMouse.dll")]
3 static extern bool Control(int nStatus, int nIndex);
4
5 //加锁
6 Control(0, 1);
7 Control(0, 0);
8
9 //解锁
10 Control(1, 1);
11 Control(1, 0);
使用全局钩子需要注意一下几点:
1、杀毒软件可能会误认为是病毒而提示或误杀程序.
2、大部分windows api会因为全局钩子而失效,PostMessage以及SendKeys.SendWait可用。
3、Windows的特殊组合键会使钩子失效,例如:连续5次的shift
4、全局钩子在多线程的情况下会有失效的情况,这种情况下,可以将全局钩子另作为一个程序,利用开启程序来控制即可(win7情况下,程序必须由管理员启动才可以发挥效果哦)。
5、在Windows WIN7 Sp1版本以后(包括SP1),全局钩子会失效。
然而在使用禁用驱动的情况下,在程序、系统当机或非正常关闭的情况下,且如果你没有为你的电脑设置可密码,那基本上只能重装电脑来解决了,但是如果有设置密码的话,可以通过其他电脑远程自己的电脑,控制电脑解除驱动禁用。还有一点需要注意,此类库的驱动禁用在设备管理里面是没办法显示的,也就是驱动已经禁用了,但是在设备管理里面没有禁用的显示,而且可以再次禁用,并且启用后,鼠标键盘仍然是失效的,必须要删除驱动重新添加才行哦,相当的变态。
正是因为禁用驱动对于用户会变得难以忍受,于是不得不寻找一种方案,来让电脑重启或者在禁用超时的情况下,可以自动解锁,因此windows服务自然就是首选啦。windows服务我就不详细说明了,因为博客园内有不少好文章可以提供哦。
我实现的方案是让windows服务监控程序的目录(公司的软件安装的具体路径),在模拟键盘鼠标之前,在程序目录下生成一个文件,然后在模拟结束之后删除此文件,windows服务会监控文件的操作动作,当发现文件创建后,开始计时,如果超出定时时间或者计算机重新启动,则会判断如果文件仍然存在的话则会进行解锁的动作。具体代码如下:
1 #region 变量
2
3 [DllImport("StopKbMouse.dll")]
4 static extern bool Control(int nStatus, int nIndex);
5
6 static readonly string FILE_NAME = "Lock.txt";
7
8 Timer timer;
9
10 int nowCount = 0;
11
12 int maxCount = 10;
13
14 bool watch = true;
15
16 #endregion
17
18 #region 属性
19
20 FileInfo File
21 {
22 get
23 {
24 return new FileInfo(string.Format("{0}\\{1}", this.fileWatch.Path, FILE_NAME));
25 }
26 }
27
28 #endregion
29
30 #region 构造函数
31
32 public UnlockKeyBoardMouse()
33 {
34 InitializeComponent();
35 this.timer = new Timer();
36 this.timer.Interval = 1000;
37 this.timer.Elapsed += TimerEscap;
38 }
39
40 #endregion
41
42 #region 事件
43
44 protected override void OnStart(string[] args)
45 {
46 if (this.File.Exists)
47 {
48 Control(1, 1);
49 Control(1, 0);
50 }
51 }
52
53 protected override void OnStop() { }
54
55 private void fileWatch_Created(object sender, FileSystemEventArgs e)
56 {
57 if (e.Name == FILE_NAME)
58 {
59 using (FileStream fs = new FileStream(@"d:\ahlTest.txt", FileMode.OpenOrCreate, FileAccess.Write))
60 {
61 using (StreamWriter sw = new StreamWriter(fs))
62 {
63 sw.BaseStream.Seek(0, SeekOrigin.End);
64 sw.WriteLine("标识文件{0}出现于:{1}\n", e.FullPath, DateTime.Now.ToLongTimeString());
65 }
66 }
67 this.nowCount = 0;
68 this.timer.Start();
69 }
70 }
71
72 private void fileWatch_Deleted(object sender, FileSystemEventArgs e)
73 {
74 if (this.watch && e.Name == FILE_NAME)
75 {
76 this.nowCount = 0;
77 this.timer.Stop();
78 }
79 }
80
81 void TimerEscap(object sender, ElapsedEventArgs e)
82 {
83 if (this.File.Exists)
84 {
85 if (this.maxCount <= ++this.nowCount)
86 {
87 this.timer.Stop();
88 Control(1, 1);
89 Control(1, 0);
90 this.watch = false;
91 this.File.IsReadOnly = false;
92 this.File.Delete();
93 using (FileStream fs = new FileStream(@"d:\ahlTest.txt", FileMode.OpenOrCreate, FileAccess.Write))
94 {
95 using (StreamWriter sw = new StreamWriter(fs))
96 {
97 sw.BaseStream.Seek(0, SeekOrigin.End);
98 sw.WriteLine("解锁键盘鼠标于:{0}\n", DateTime.Now.ToLongTimeString());
99 }
100 }
101 }
102 }
103 else
104 {
105 this.timer.Stop();
106 }
107 }
108
109 #endregion
以上便是我在此次更改公司模块所做的尝试,有不好之处请指出,不吝赐教哦。