设计思想

从本质上来说这个项目不算机器人,因目前不能提供自动回复、自动对话的业务场景。

需求说明

公司大部分业务都是ToB的,运营管理着好几百个微信群,产品跟运营私交甚好,因此,在产品的耳边吹了一口气,“我们做一个微信机器人,帮助我们管理微信群吧,最好加上群聊,也可回答一些客户的问题”。产品听了,觉得这事可以有,想想,快到年底了,KPI考核也没什么补救,说不定这也就是一部分。

时间倒退到2017年8月底,产品开始设计一款名叫“微信机器人”的项目。嗯,看名字就觉得很高大上。BTW,设计稿呢???没有,就是口头产品,没进入评审环境,直接告诉我我们需要一款这样的…,能干…最好…的产品,Balabala说了一堆,其实就是做一个能够收发群消息,能够管理群成员,能够干微信能干的事情,最后,这些要干的事情,必须在我们的运营管理后台可操作,消息发送状态可查看

这项殊荣交给我了,限定要在9月初上线。

服务设计

拿到需求之后,并没有急着开发,而是先度娘问问,有没有现成的,如果有的话,直接copy一份也就成了(毕竟人家说我们是搬砖的)。调研后得知,大家都基于微信web版开发的,在GitHub上发现有一堆微信机器人项目,下载了一份Python的demo,直接在本地运行,发现没问题。但是业务肯定要修改,而且,我对Python并不是很熟悉,于是找了一份Java版本的。

读Java版本的源码,再加上自己使用Charles抓取微信的web版,发现流程都一致(图1)。但是处理业务的逻辑有很大的不同。

图1 web微信流程图
![][1]
网络上存在的微信机器人项目,都是开启单个守护线程,只能有一个用户登录,且不能通过提供API或者RPC接口。因此,根据业务的要求,重新设计了一套流程(图2)。
图1 web微信流程图
![][2]

服务设计也相对简单,和调用web API的流程基本也保持一致,最多也就是增加了一些自己的业务的逻辑设计。设计的过程也是几经变化逐步修改过来的。刚开始,微信反爬没有那么严格,就在第一个版本上线之后,发现微信的安全策略做的着实好,无论我发消息的频率阈值范围设置自我感觉多么合理的时候,微信服务器依然能够判定我就是机器人,web版登录还是被封,此是其一。其二,频繁的登录,或者说登录间隔在5分钟之内,都是synccheck接口返回的retcode为1101,所以,在监听事件处理方面的设计做了优化。

本月初(2017-12),微信封杀web版机器人的政策开始变得严格起来。线上项目基本还没怎么用,就出现被封的情况。眼看,此项目就要流产了,leader建议用类似按键精灵这种方式来群发消息。hn~项目也应该由“微信机器人”变为“微信群发助手”。写文章的这几天,看了wetool这样一款软件,写的很不错,但是没办法拿到源码或者说没有API调用,否则我就用它了。

设计思路也就需要改变了,从调用API变成了操作UI,可以说处于一个UI自动化测试的流程阶段(我还是有点气愤的说,老子不是搞测试的)。通过操作微信客户端界面,在搜索框中,输入群名,等待匹配群名,点击群名进入群聊窗口,在光标处粘贴剪贴板中的消息内容,点击发送(或者按Enter键)。最最重要的多用户登录解决方案是 在Windows server2012中安装虚拟机,每个虚拟机中装win7,在win7中安装微信客户端以及运行环境,每隔虚拟机配置网桥,绑定独立内网IP,运营和ip做绑定调用即可。至于,这些的实现,需要根据要求自己去琢磨。按键精灵可以快速的实现该功能,但是按键精灵不提供API,无法通过API调用。Sikulix是一款图像编程脚本,有API且有Java版本。

功能实现

因为我们需要直接在运营后台调用接口,发送服务消息。所以,我选择了sikuli脚本编程。开发环境是Mac,部署环境是Windows,所以代码兼容了Mac和Windows。

打开微信界面

openWechatDesktop将已经打开的微信客户端窗口置于最前面,并判断是否有客户端存在。对于Windows系统而言,App.focus方法并不一定能让微信窗口置于最前面(测试发现,属于概率事件),所以直接返回true。getAppWindow判断是否有微信的窗口,微信登录窗口和微信聊天窗口对于App.focus而言,都是运行中的窗口,因此,凭借App.focus不能判断微信聊天窗口是否打开,因此getAppWindow方法对于已经存在的窗口做判定。但是对于Windows系统,调用Windows的消息处理句柄来判断窗口的存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private boolean openWechatDesktop() {
boolean isHasWindow = false;
// 在Windows Server 2012中检测不到应用软件
/*if (!app.isRunning()) {
Debug.error("%s应用未打开", APP_NAME);
return isHasWindow;
}*/
if (Settings.isMac()) {
App.focus(APP_NAME);
isHasWindow = true;
} else if (Settings.isWindows()) {
isHasWindow = true;
}
return isHasWindow;
}

private boolean getAppWindow(String appName, String formClass, String formTitle) {
if (Settings.isMac()) {
String scanLogin = "scanlogin.png";
String confirmLogin = "confirmlogin.png";
Screen screen = new Screen();
return screen.exists(scanLogin) == null && screen.exists(confirmLogin) == null;
} else if (Settings.isWindows()) {
return TryWithHwnd.setForegrundWindows(formClass, null);
}
return false;
}

setForegrundWindows调用Windows的user32接口,通过窗口类名或者窗口标题来获取句柄,并前置窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static boolean setForegrundWindows(String className, String windowName) {
if (StringUtils.isEmpty(className) && StringUtils.isEmpty(windowName)) {
return false;
}
WinDef.HWND hwnd;
if (StringUtils.isEmpty(className)) {
hwnd = User32.INSTANCE.FindWindow(null, windowName); // 第一个参数是Windows窗体的窗体类,第二个参数是窗体的标题
} else if (StringUtils.isEmpty(windowName)) {
hwnd = User32.INSTANCE.FindWindow(className, null);
} else {
hwnd = User32.INSTANCE.FindWindow(className, windowName);
}
if (hwnd == null) {
Debug.info("应用没有运行");
return false;
} else {
User32.INSTANCE.ShowWindow(hwnd, 9); // SW_RESTORE
User32.INSTANCE.SetForegroundWindow(hwnd); // bring to front
User32.INSTANCE.ShowWindow(hwnd, SW_MAXIMIZE);
return true;
}
}

微信客户端是否运行

isRunning方法主要是执行Linux或者Windows命令,从进程列表中货物微信是否在运行。当然,这地方Windows的命令wmic是有坑点,至于是什么问题,在坑点说明中详细解释以及称述解决办法。

execWinCmdexecCmd用于执行Windows的命令和其他命令。其实另个的作用基本一样,但是传入的参数可能会有所不同,以及对wmic命令的Java执行有点不同(也就是上面说的坑点)。

runWeChat打开微信的登录窗口。主要用于当我们检测微信没有打开或者微信进程不存在的时候,我们通过命令直接调用可执行文件或者脚本,启动应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void isRunning() {
List<String> cmd = new ArrayList<>();
if (Settings.isMac()) {
cmd.add("/bin/sh");
cmd.add("-c");
cmd.add("ps -ef | grep WeChat | awk '{print $8}'");
} else if (Settings.isWindows()) {
cmd.add("cmd.exe /c wmic process where caption='WeChat.exe' get commandline /value");
} else {
cmd.add("/bin/sh");
cmd.add("-c");
cmd.add("ps -ef | grep WeChat");
}

List<String> result = Settings.isWindows() ? execWinCmd(cmd.get(0)) : execCmd(cmd);
if (!existsProcessor(APP_NAME, result)) {
// 打开微信登录界面
Debug.info("微信应用没有打开");
runWeChat(APP_NAME);
}
}

private void runWeChat(String appName) {
if (StringUtils.isEmpty(appName)) {
Debug.error("不支持的操作系统");
return;
}
App app = new App(appName);
app.open();
}

输入群名匹配群

和设计原理中说明的一样,我们是通过输入群名称来查找群名。但是如何输入群名称,以及在什么地方输入,鼠标是不知道,我们必须找到输入框,通过点击确定光标聚焦之后,才能输入文本。

或许有Windows开发经验的同学会说,怎么不用Windows的组件的消息句柄呢?!是的,一开始也是这么想的,但是经过调试发现,微信的Windows客户端UI是基于DirectUI封装的(Windowsless或者Handleless,也就是无句柄窗口),除了登录窗口以及微信登录之后的主窗口之外,其他均为DirectUI,Soga,没办法通过句柄定位输入框。

既然我们选择的sikulix,那么我们就通过图片识别来确认搜索框的位置,找到该区域之后再做点击、光标聚焦等事件操作。当然了,处理一般的按键类操作,都需要等待设备的响应,也就是需要我们等待几百毫秒。

输入群名称,匹配到群之后,我们单击第一个匹配的群,打开聊天窗口。这地方可能需要注意,群名支持最小匹配,所以,必须要想办法让匹配的群名称是唯一的且不是最小匹配单元。什么意思呢,就是说输入“测试群1”和输入“测试群123"打开的群都有可能是测试群123。

Mac和Windows的处理方式也有点不同,原因是在Windows中,我们打开微信客户端聊天界面的时候通过主窗口句柄执行了最大化,所以可以直接根据绝对坐标值搜索输入框,但是mac没有实现最大化,只能通过相对位置定位搜索输入框的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private boolean findSearchInput(String groupName) {
Screen screen = new Screen();
Region region = Region.create(0, 0, getScreenX(), getScreenY() / 2);
screen.capture(region);
Match target = null;
if (Settings.isMac()) {
target = screen.exists("max.jpg");
if (target == null) {
target = screen.exists("search2.jpg");
}
} else if (Settings.isWindows()) {
return findGroupForWin(groupName);
}

if (target != null) {
Location location = target.getTarget();
Debug.info("找到微信搜索框x轴:%s", location.x);
Debug.info("找到微信的搜索框y轴:%s", location.y);
return findGroupForMac(groupName, target);
}

return false;
}

private boolean findGroupForMac(String groupName, Match target) {
if (StringUtils.isEmpty(groupName) || target == null && target.getScore() > 0.8) {
return false;
}
Location location = target.getTarget();
Location loc = location.right(55);
Match match = new Match(target);
match.setLocation(loc);
match.setY(location.getY() - 10);
match.click();
match.type(Key.TAB);
sleep(1000);
match.paste(groupName);

// 查看是否有群
match.setX(match.getX() + 35);
match.setY(match.getY() + 80);
match.setW(200);
Region region = Region.create(match.x, match.y, match.w, match.h);

return OpenChatRoom(region, groupName);

}

private boolean findGroupForWin(String groupName) {
if (StringUtils.isEmpty(groupName)) {
return false;
}
Location location = new Location(75, 25);
Screen screen = new Screen();
screen.setLocation(location);
screen.setH(25);
screen.setW(190);
screen.click();
sleep(100);
// screen.type(Key.TAB);
// sleep(100);
// screen.click();
sleep(1000);
screen.paste(groupName);
sleep(2000);
// 查看是否有群
screen.setY(screen.getY() + 60);
screen.setH(60);
screen.setW(200);
return OpenChatRoom(screen, groupName);
}

打开微信群聊天窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private boolean OpenChatRoom(Region region, String groupName) {
if (region == null || StringUtils.isEmpty(groupName)) {
return false;
}
if (Settings.isMac()) {
Screen screen = new Screen();
screen.setRect(region.x - 10, region.y - 10, 90, 30);
sleep(1000);
Match target = screen.exists("no_match.jpg");
if (target != null) {
Debug.error("没有匹配到群:【%s】", groupName);
return false;
}
} else if (Settings.isWindows()) {
LOGGER.info("查询微信群: {}", groupName);
region.click();
WinDef.HWND hwnd = TryWithHwnd.getWindowHwhd("FTSMsgSearchWnd");
if (hwnd != null) {
LOGGER.error("微信群不存在:{}", groupName);
TryWithHwnd.closeWindowHwhd(hwnd);
return false;
}
}
region.click();
return true;
}

通过句柄关闭搜索群,弹出的未找到群的搜索框:

1
2
3
4
5
6
7
public static void closeWindowHwhd(HWND hwnd) {
if (hwnd != null) {
//MyUser32.INSTANCE.SendMessage(hwnd, new WinDef.UINT(0x010), new WinDef.DWORD(0x00), new WinDef.DWORD(0x00));
User32.INSTANCE.PostMessage(hwnd, WM_CLOSE, new WinDef.WPARAM(0x00), new WinDef.LPARAM(0x00));
}
}

发送消息

打开聊天窗口之后,我们把传入的字符串交给sikulix,通过sikulix的paste方法粘贴。但在粘贴之前,我们需要找到鼠标光标的位置(其实这一步可以不需要),消息粘贴之后,执行enter键发送,注意发送的频率,过快依然存在强制退出的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private boolean sendMsg(String msg) {
if (StringUtils.isEmpty(msg)) {
return false;
}
if (Settings.isMac()) {
int x = MouseInfo.getPointerInfo().getLocation().x;
int y = MouseInfo.getPointerInfo().getLocation().y;
Screen screen = new Screen();
screen.setRect(x, y, getScreenX() - x, getScreenY() - y);
Match target = screen.exists("input_emotion.jpg");
if (target != null) {
target.y = target.y + target.h;
target.click();
target.paste(msg);
sleep(getRandom(1000, 5000));
target.keyDown(Key.ENTER);
target.keyUp();
}
} else if (Settings.isWindows()) {
WinDef.HWND hwnd = TryWithHwnd.getWindowHwhd(FORM_CLASS);
if (hwnd != null) {
Screen screen = new Screen();
screen.setRect(315, getScreenY() - 140, getScreenX() - 315, 140);
screen.highlight(3);
screen.paste(msg);
sleep(getRandom(1000, 5000));
screen.keyDown(Key.ENTER);
screen.keyUp();
}
}
return true;
}

public static HWND getWindowHwhd(String className) {
if (StringUtils.isEmpty(className)) {
return null;
}
return User32.INSTANCE.FindWindow(className, null);
}

打包部署

项目基于Maven构建,使用maven-compiler-plugin插件编译,使用的是maven-war-plugin插件打包,其他插件用于单元测试以及对项目clean等。我们这里只说组装包到部署阶段的一些处理方式。

这里面涉及了几个变量,如果后面遇到了,其代表的意思和此处表达的一样,替换的值也是保持一致的:

  • ${environment.dir} 指的是配置文件的目录,我们在父工程下面创建了一个目录,存放各种环境的配置文件
  • ${environment} 指的是环境变量(dev,test,production),但是这里我们是把配置文件properties和环境关联在一起的,比如开发环境是dev.properties。
  • ${java.version} Jdk的版本,设置的是1.8

依赖配置

这里面我们在parent pom.xml中增加部署包的组装,组装插件maven-assembly-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!-- parent pom -->
<build>
<filters>
<filter>../${environment.dir}${environment}.properties</filter>
</filters>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.3.0.v20150612</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>

新建assembly模块

在我们的项目中增加一个子模块叫new-wxbot-assembly,我们按照maven-assembly-plugin的配置来写相关的代码。

下图是该模块的工程结构图(图3),assembly是插件的配置xml,bin使我们的相关脚本,启动和关闭Tomcat的脚本,config目录是一个配置目录,包含一个deploy.properties文件,保存了本地Tomcat的路径,加jdk的路径,还有一些Tomcat的端口的配置,这些配置都是用于在构建脚本的时候替换里面的参数。tomcat目录是Tomcat的虚拟目录配置。

图3 assembly结构图
![][3]

在assembly模块的pom.xml中做一些清理工作,并指定maven-assembly-plugin的描述文件等相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!-- assembly pom -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>new-wxbot</artifactId>
<groupId>com.zhoujunwen</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>new-wxbot-assembly</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>../deploy</directory>
<includes>
<include>**/*</include>
</includes>
</fileset>
<fileset>
<directory>..</directory>
<includes>
<include>deploy.tar.gz</include>
</includes>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>deploy</finalName> <!-- 这里需要注意,最终组装的包名 -->
<outputDirectory>../</outputDirectory>
<filters>
<filter>../${environment.dir}${environment}.properties</filter>
</filters>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/assembly/release.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

assembly的描述文件是一个集合描述符,每一项都指定了源文件的目录,输入目录,以及过滤的文件名称。指的注意的是,pom中的finalName配置,该配置项指定了最终组装的包的名称,我们这里是deploy,而assembly描述符中生成的输出目录都在改目录下面

现在我们看看assembly文件的配置描述,directory为.的描述是把assembly模块中的所有文件都放在tomcat/temp目录中,src/bin是把当前目录下src/bin中的文件输出到bin目录,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<assembly>
<id>dist</id>
<formats>
<format>dir</format>
<format>tar.gz</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>.</directory>
<outputDirectory>tomcat/temp</outputDirectory>
<excludes>
<exclude>*/**</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>src/bin</directory>
<outputDirectory>bin</outputDirectory>
<includes>
<include>**/*</include>
</includes>
<fileMode>0755</fileMode>
<filtered>true</filtered>
</fileSet>
<fileSet>
<directory>src/conf</directory>
<outputDirectory>conf</outputDirectory>
<includes>
<include>**/*</include>
</includes>
<filtered>true</filtered>
</fileSet>
<fileSet>
<directory>src/tomcat/conf</directory>
<outputDirectory>tomcat/conf</outputDirectory>
<includes>
<include>**/*</include>
</includes>
<filtered>true</filtered>
</fileSet>
<fileSet>
<!-- 复制new-wxbot-web模块打的war包到target目录 -->
<directory>../new-wxbot-web/target</directory>
<outputDirectory>target</outputDirectory>
<includes>
<include>new-wxbot.war</include>
</includes>
<filtered>false</filtered>
</fileSet>
<fileSet>
<!-- 复制sikulix所需要的图片 -->
<directory>../build/images</directory>
<outputDirectory>sikuli/images</outputDirectory>
<includes>
<include>**/*</include>
</includes>
<filtered>true</filtered>
</fileSet>
</fileSets>
</assembly>

mac系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#startup
#!/bin/bash

cd `dirname $0`
BIN_DIR=`pwd`
cd ..

export LOG_PATH=/Users/zhoujunwen/data/logs/new-wxbot

export APP_DEPLOY_HOME=`pwd`
export JAVA_HOME=`sed '/java\.home/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`
export TOMCAT_HOME=`sed '/tomcat\.home/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`
export APP_PORT=`sed '/app\.port/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`
export TOMCAT_LOG=$LOG_PATH/tomcat.log
export CATALINA_OUT=/dev/null
export PATH=$JAVA_HOME/bin:$PATH


PRODUCTION_MODE=`sed '/env/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`
APP_DEBUG_PORT=`sed '/debug\.port/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`
APP_REMOTE_PORT=`sed '/remote\.port/!d;s/.*=//' ${APP_DEPLOY_HOME}/conf/deploy.properties | tr -d '\r'`


if [ ! -e $JAVA_HOME ]; then
echo "********************************************************************"
echo "**Error: $JAVA_HOME not exist"
echo "********************************************************************"
exit 1
fi

if [ ! -e $TOMCAT_HOME ]; then
echo "********************************************************************"
echo "**Error: $TOMCAT_HOME not exist."
echo "********************************************************************"
exit 1
fi

JAVA_OPTS_EXT=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8 -DdisableIntlRMIStatTask=true -Ddubbo.application.logger=slf4j"
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=$APP_DEBUG_PORT,server=y,suspend=n "

inet_ip=`/sbin/ifconfig|grep inet|grep -v inet6|grep -v 127.0.0.1|awk '{if(substr($2,1,5)=="addr:"){print substr($2,6)} else{print $2}}'|head -n 1`
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=$APP_REMOTE_PORT -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=$inet_ip"


JAVA_GC_LOG_OPTS=" -XX:+PrintGCDateStamps -verbose:gc -XX:+PrintGCDetails -Xloggc:$LOG_PATH/gc -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M "
JAVA_DUMP_OPTS=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$LOG_PATH/dump_\${date}.hprof "

JAVA_ERROR_OPTS=" -XX:ErrorFile=$LOG_PATH/hs_err_pid%p.log "

if [ $PRODUCTION_MODE = "new-wxbot" ]; then
JAVA_MEM_OPTS=" -server -Xmx4096m -Xms2048m -Xmn1g -XX:PermSize=256m -XX:MaxPermSize=512m -Xss256k -XX:+UseConcMarkSweepGC -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime "

JAVA_OPTS=" $JAVA_MEM_OPTS $JAVA_GC_LOG_OPTS $JAVA_DUMP_OPTS $JAVA_ERROR_OPTS "
else
JAVA_MEM_OPTS=" -server -Xms512m -Xmx512m -XX:PermSize=256m -XX:SurvivorRatio=2 -XX:+UseParallelOldGC "
JAVA_OPTS=" $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS $JAVA_GC_LOG_OPTS $JAVA_DUMP_OPTS $JAVA_ERROR_OPTS "
fi

JAVA_OPTS=" $JAVA_OPTS -Djsse.enableSNIExtension=false"
echo "********************************************************************"
echo "**ENV: $PRODUCTION_MODE"
echo "********************************************************************"

export JAVA_OPTS=" $JAVA_OPTS $JAVA_OPTS_EXT"


export CATALINA_BASE=$APP_DEPLOY_HOME/tomcat
export CATALINA_PID=$CATALINA_BASE/tomcat.pid

if [ ! -d "$CATALINA_BASE/conf/Catalina/localhost" ]; then
mkdir -p $CATALINA_BASE/conf/Catalina/localhost
fi

chmod -R +x $APP_DEPLOY_HOME/bin/
$APP_DEPLOY_HOME/bin/tomcatctl start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# tomcatctl
#!/bin/bash

DATE=`date +%Y-%m-%d`
OS=`uname`

export LOG_PATH=/Users/zhoujunwen/data/logs/new-wxbot

start()
{
if [ -f "$CATALINA_BASE/tomcat.pid" ]; then
echo "warn: App is already run, please check."
exit;
fi

STR=`netstat -an | grep "$APP_PORT" | grep LISTEN`
if [ ! -z "$STR" ]; then
echo "warn: Tomcat port is already used, please check."
exit;
fi

if [ ! -d "$LOG_PATH" ]; then
mkdir -p $LOG_PATH
fi

$TOMCAT_HOME/bin/startup.sh >$TOMCAT_LOG 2>&1 &
starthttpd
}

stop()
{
if [ ! -f "$CATALINA_BASE/tomcat.pid" ]; then
echo "App process is not exist!"
exit 1;
fi

if [ ! -d "$LOG_PATH" ]; then
mkdir -p $LOG_PATH
fi

TIMESTAMP=`date +%Y_%m_%d_%H:%M`
KILL_LOG=$LOG_PATH/kill.log

echo "`hostname` was stopted at $TIMESTAMP" >>$KILL_LOG

pid=`cat $CATALINA_BASE/tomcat.pid`
kill -9 $pid
sleep 5
str=`ps -p $pid|grep $pid`
if [ -z "$str" ]; then
echo "APP $pid Shutdown is ok!"
rm -f $CATALINA_BASE/tomcat.pid
else
echo "APP $pid Shutdown is failed!"
echo "Please kill pid $pid manually ,and romove file $CATALINA_BASE/tomcat.pid"
fi
}

starthttpd()
{
STARTTIME=`date +"%s"`
COUNT=0
sleep 5
while true
do
RESULT=`curl --connect-timeout 1 -s http://127.0.0.1:8088/ok.htm`
ENDTIME=`date +"%s"`
COSTTIME=$(($ENDTIME - $STARTTIME))

if [ -z "$RESULT" ]; then
sleep 1
echo -n -e "\rWait Tomcat Start: $COSTTIME seconds"
continue
fi

COUNT=`echo $RESULT | grep -c -i ok`
if [ $COUNT -ge 1 ]; then
pid=`cat $CATALINA_BASE/tomcat.pid`
echo "APP $pid Start in $COSTTIME seconds."
return
else
echo "ERROR: Start APP Failed!!!"
exit
fi
done
}

usage() {
echo "Usage: xxx {start|stop|restart}"
exit 1;
}


case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 5
start
;;
*)
echo $ACTION
usage
;;
esac

win系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#startup.bat
@echo off
rem Windows启动服务脚本
rem ---------------------------------------------------------------------------
rem Start script for the CATALINA Server
rem ---------------------------------------------------------------------------

setlocal
rem 处理环境变量,替换配置文件
set "CURRENT_DIR=%cd%"
cd ..

set "LOG_PATH=${deploy.log.path}"
set "APP_DEPLOY_HOME=%cd%"

for /f "tokens=1* delims='='" %%a in (%APP_DEPLOY_HOME%\conf\deploy.properties) do (
echo;%%a|find "java.home">null&&set JAVA_HOME=%%b
echo;%%a|find "tomcat.home">null&&set TOMCAT_HOME=%%b
echo;%%a|find "app.port">null&&set APP_PORT=%%b
echo;%%a|find "env">null&&set PRODUCTION_MODE=%%b
echo;%%a|find "debug.port">null&&set APP_DEBUG_PORT=%%b
echo;%%a|find "remote.port">null&&set APP_REMOTE_PORT=%%b
)

if "%JAVA_HOME%" == "" (
echo ********************************************************************
echo **Error: JAVA_HOME is empty
echo ********************************************************************
exit /b
)

if not exist "%JAVA_HOME%" (
echo ********************************************************************
echo **Error: %JAVA_HOME% not exist
echo ********************************************************************
exit /b
)

if "%TOMCAT_HOME%" == "" (
echo ********************************************************************
echo **Error: TOMCAT_HOME is empty.
echo ********************************************************************
exit /b
)

if not exist "%TOMCAT_HOME%" (
echo ********************************************************************
echo **Error: %TOMCAT_HOME% not exist.
echo ********************************************************************
exit /b
)

set "JAVA_HOME=%JAVA_HOME:\\=\%"
set "TOMCAT_HOME=%TOMCAT_HOME:\\=\%"

set "TOMCAT_LOG=%LOG_PATH%\tomcat.log"
set "CATALINA_OUT=null"
set "PATH=%JAVA_HOME%\bin;%PATH%"

rem "设置JVM相关参数"
rem "配置环境参数变量"
set "JAVA_OPTS_EXT=%JAVA_OPTS_EXT% -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF8 -DdisableIntlRMIStatTask=true -Ddubbo.application.logger=slf4j"
set "JAVA_DEBUG_OPTS=%JAVA_DEBUG_OPTS% -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=%APP_DEBUG_PORT%,server=y,suspend=n"

ver|findstr "5.1" >nul && (
set "m=ipconfig^|findstr /i "ip address""
)|| (
set "m=ipconfig^|findstr /i "ipv4""
)
for /f "tokens=14* delims=: " %%1 in ('%m%')do (
set "IPV4=%%2" goto okIpNet
)
:okIpNet
set "inet_ip=%IPV4%"
set "JAVA_JMX_OPTS=%JAVA_JMX_OPTS% -Dcom.sun.management.jmxremote.port=%APP_REMOTE_PORT% -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=%inet_ip%"

set "JAVA_GC_LOG_OPTS=%JAVA_GC_LOG_OPTS% -XX:+PrintGCDateStamps -verbose:gc -XX:+PrintGCDetails -Xloggc:%LOG_PATH%\gc -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M "
set "JAVA_DUMP_OPTS=%JAVA_DUMP_OPTS% -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%LOG_PATH%\dump_\%date:~0,4%.%date:~5,2%.%date:~8,2%.hprof "

set "JAVA_ERROR_OPTS= -XX:ErrorFile=%LOG_PATH%\hs_err_pid%p.log "

if "%PRODUCTION_MODE%" == "octopus-new-wxbot" (
set "JAVA_MEM_OPTS= -server -Xmx4096m -Xms2048m -Xmn1g -XX:PermSize=256m -XX:MaxPermSize=512m -Xss256k -XX:+UseConcMarkSweepGC -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime "

set "JAVA_OPTS=%JAVA_MEM_OPTS% %JAVA_GC_LOG_OPTS% %JAVA_DUMP_OPTS% %JAVA_ERROR_OPTS% "
) else (
set "JAVA_MEM_OPTS= -server -Xms512m -Xmx512m -XX:PermSize=256m -XX:SurvivorRatio=2 -XX:+UseParallelOldGC "
set "JAVA_OPTS=%JAVA_MEM_OPTS% %JAVA_DEBUG_OPTS% %JAVA_JMX_OPTS% %JAVA_GC_LOG_OPTS% %JAVA_DUMP_OPTS% %JAVA_ERROR_OPTS% "
)

set "JAVA_OPTS=%JAVA_OPTS% -Djsse.enableSNIExtension=false"
echo ********************************************************************
echo **ENV: %PRODUCTION_MODE%
echo ********************************************************************

set "JAVA_OPTS=%JAVA_OPTS% %JAVA_OPTS_EXT%"
set "CATALINA_BASE=%APP_DEPLOY_HOME%\tomcat"
set "CATALINA_PID="CATALINA_BASE\tomcat.pid"
set "CATALINA_HOME=%TOMCAT_HOME%"

if not exist "%CATALINA_BASE%\conf\Catalina\localhost" (
md "%CATALINA_BASE%\conf\Catalina\localhost"
)

call "%APP_DEPLOY_HOME%\bin\tomcatctl.bat"
:end

调用Tomcat的启动命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# tomcatctl.bat
setlocal

rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

rem Check that target executable exists
if exist "%EXECUTABLE%" goto okExec
echo Cannot find "%EXECUTABLE%"
echo This file is needed to run this program
goto end
:okExec

rem Get remaining unshifted command line arguments and save them in the
set CMD_LINE_ARGS=
:setArgs
if ""%1""=="""" goto doneSetArgs
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
shift
goto setArgs
:doneSetArgs

call "%EXECUTABLE%" run %CMD_LINE_ARGS%

:end

Windows设置

默认情况下,当用户没有在 Windows 上执行任何输入(没有鼠标键盘等的输入)并保持一定时间后,Windows 会自动切换到锁屏模式(或屏保模式),甚至待机。

一般情况下,这样不会有任何问题,而且也是推荐的设置(出于安全和节能的角度)。但是,如果这台电脑被用于进行一些自动化的测试,尤其是涉及到 UI 的交互操作时(比如,用脚本操控鼠标来模拟点击一个按钮),这将会是个很大的问题:鼠标键盘失效了!

解决这个办法的方案很简单:设置 Windows 的电源模式,让 Windows 不要自动锁屏和待机,同时去掉屏保。

基础概念阐述

我们需要了解3个在Windows操作系统中的经常用到却鲜为人知的对象:Session(会话)、Windows Station(工作站) 和 Desktop(桌面)。

Session

session表示用户会话,每个登录操作系统的用户都会分配一个唯一的登录会话,用于标识该用户。操作系统(Vista 及以上)保留0号会话给一些系统服务及用户态的驱动使用,第1个登录系统的用户使用的 Session ID 为1,该用户执行的所有应用程序都在 Session 1 下执行。

我们可以打开任务管理器,切换到进程列表,然后在菜单->视图->选择列中,勾选 Session ID 列。

![][4]

如果有其它的用户登录到系统,就会看到 Session ID 大于1的情况,比如远程桌面。

Windows Station

Station可以理解为工作站,它被认为是桌面和进程的安全边界。因此,每一个 session 都会包含多个 station,而每一个 Station 又包含1个或多个 Desktop。

但是,多个 Station 中,只有名字叫 Winsta0 的 Station 才是交互式的 station,也就是说只有它才能有 UI 并接受用户输入。所以每个 Session 都有一个叫 Winsta0 的用户进行交互。

Desktop

这里的Desktop并不是我们进入系统后所看到的那个蓝底的桌面(,我们看到的这个桌面,实际上只是一个窗口)。而是逻辑上的一个显示对象,它包含可显示的 GUI 对象(比如窗口、菜单、钩子等)。一般情况下,WinSta0 包含至少三个Desktop:登录(WinLogon)、屏保(ScreenSaver)和默认(Default,能看到所有应用程序的地方)。

同一时刻只能有一个Desktop处于激活状态(而能激活的 Desktop 只能属于 Winsta0)。用户还没登录的时候,登录桌面处于激活状态。登录之后,默认的Desktop处于激活状态。当达到系统电源设置的某个点的时候,系统切换到屏保,此时屏保Desktop处于激活状态。当用户按下 Ctrl + Alt + Delete 时,切换到登录Desktop,此时登录Desktop处于激活状态。

激活状态的Desktop才能接收用户输入,钩子才能获取其中的某个窗口消息。

如果你已经体验过 Win10,那应该就会知道 Win10 提供了很方便的创建多个Desktop的方式。

问题点处理

最小化问题

最小化会让远程桌面的会话切换到无图形界面的模式,这自然就无法继续接收鼠标、键盘的指令了。

断开远程桌面问题

关闭远程桌面会让系统切换到登录Desktop的界面,而在该Desktop上并没有我们打开的其它窗口,因此会导致 UI 自动化测试失败。

解决办法

最小化

通过设置注册表的值可以阻止切换到无图形界面。

  • 32位系统
    找到 HKEY_LOCAL_MACHINESoftwareMicrosoftTerminal Server Client,创建一个 DWORD 类型的值,名字叫做RemoteDesktop_SuppressWhenMinimized,然后设置值为 2。

  • 64位系统
    找到 HKEY_LOCAL_MACHINESoftwareWow6432NodeMicrosoftTerminal Server Client,然后和 32 位一样创建一个DWORD 类型的值,名字叫做 RemoteDesktop_SuppressWhenMinimized,并设置值为 2。

上面的改动会应用于整个机器,如果只想应用于当前的用户,把 LOCAL 替换成 USER。

关闭远程桌面

在远程桌面(被连接到的电脑)中先执行 query session 来查看当前登录到的 session,(远程桌面的 sessionName 都以 rdp-tcp 开头)。

![][5]

然后用管理员用户打开命令行工具,并执行 “tscon rdp-tcp#0 /dest:console”,其中 rdp-tcp#0 为该该命令会关闭远程桌面的连接,然后把连接返回给远程的那台电脑(绕开登录过程)。这里的 console 只是一个 session 的名字,而这个名字的意思并非是 C# 中 “控制台” 的意思,而是指带有输入输出设备的机器,一般直接登录电脑的会话就是 console。

假设电脑A执行 mstsc 连接到电脑B(,连接成功后,电脑B黑屏),此时在电脑B上执行上述命令后(替换对应的session名字),电脑A中的远程连接窗口会被关闭,并提示远程连接会话已经终止。电脑B(假设运行在另一台物理机上)会恢复到已经登录的状态,如果需要重新让电脑B恢复锁屏状态,可以在电脑B上执行如下命令:

rundll32.exe user32.dll,LockWorkStation

坑点说明

sikulix依赖的lib包

sikulix涉及到操作系统环境相关操作,因此在不同的环境会生成不同的lib包。在mac下,依赖文件中可以看出有这么一个依赖:com.sikulix:sikulixlibsmac:1.1.1。该依赖中,有很多.dylib文件,这些文件就是sikulix操作mac系统中相关应用的依赖库。同理,在Windows中,依赖变为:com.sikulix:sikulixlibswin:1.1.1,该依赖包中有许多.dll文件,这些文件就是与Windows环境相关的库文件。

在mac上,默认路径是:/Users/${yourName}/Library/Application Support/Sikulix,在win中,默认是获取环境变量%APPDATA%的目录,该目录默认为:C:\Users\${yourName}\AppData\Roaming\Sikulix\,其中${yourName}为你的电脑当前用户名。

这些信息我们从哪里获取到呢?在Settings类中,找到getSikuliDataPath方法,里面就有相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String getSikuliDataPath() {
String home, sikuliPath;
if (isWindows()) {
home = System.getenv("APPDATA");
sikuliPath = "Sikulix";
} else if (isMac()) {
home = System.getProperty("user.home")
+ "/Library/Application Support";
sikuliPath = "Sikulix";
} else {
home = System.getProperty("user.home");
sikuliPath = ".Sikulix";
}
File fHome = new File(home, sikuliPath);
return fHome.getAbsolutePath();
}

但是很遗憾,没有类似setter方法,用于用户指定其lib目录。对于win用户而言,可以通过修改环境变量%APPDATA%可以指定自己想要存放的目录。

这里,如果打好的war包直接部署在Tomcat中启动会报错。因为所需要的lib包不存在。在开发环境中,我们通过mvn exec:exec执行一个main方法可以生成这些lib文件。但是部署环境我们没有maven环境,此时,最好在系统环境一致的情况下,将开发环境生成的相关文件复制到部署环境,路径就是上面说的getSikuliDataPath指定的路径。

sikulix图片位置设置

sikulix是一款图形编程语言,因此,对图像的依赖是不言而喻的。比如我们查找一个图片的位置find(String imagePath),就需要指定图片的绝对路径。每个需要图片的方法,我们都填充一段路径前缀,看起来也很繁杂凌乱,尤其对于代码审美要求严格的人来说,是一件很苦恼的事情。当然我们可以将其前缀设为一个静态常量,做字符串拼接。不过,sikulix非常人性化的,在Settings中增加了一个全局配置图片路径的函数:

1
2
3
4
5
6
7
8
9
10
11
/**
* create a new PathEntry from the given absolute path name and add it to the
* end of the current image path<br>
* for usage with jars see; {@link #add(String, String)}
*
* @param mainPath relative or absolute path
* @return true if successful otherwise false
*/
public static boolean add(String mainPath) {
return add(mainPath, null);
}

我们借助该函数,可以设置我们的图片路径的前缀。这样我们不用在find、exists等函数中做字符拼接了,直接填写图片的文件名即可。

开启OCR图片识别中文识别失败

OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程;即,针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,并通过识别软件将图像中的文字转换成文本格式,供文字处理软件进一步编辑加工的技术。
百科词条

说白了,就是利用该技术识别图片区域的文字,将其保存为文本。sikulix默认是关闭OCR,开启是需要执行下面的操作:

1
2
3
Settings.OcrTextSearch = true;
Settings.OcrTextRead = true;
Setting.OcrLanguage = "chi_sim";

OcrLanguage默认识别英文,对于中文,需要安装中文训练数据包:chi_sim.traineddata,网上教程很多,我配置之后依然不能正确识别中文,命令行中倒是能识别中文,但准确率不高。

因此,不建议大家使用Sikulx中的OCR功能。

Java执行Windows的wmic命令不起作用

Java执行命令行的代码无非就是:Runtime.getRuntime().exec(cmd)new ProcessBuilder(cmd).start()这两种。接收参数可以是字符串,也可以是字符串数组。

什么时候使用字符串,什么时候使用数组呢?

一般而言,如果命令中只有简单的单条命令,就可以使用字符串。如果命令行中出行管道流(grep)等,需要使用数组,具体的命令作为参数传给执行程序。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
List<String> cmd = new ArrayList<>();
if (Settings.isMac()) {
cmd.add("/bin/sh");
cmd.add("-c");
cmd.add("ps -ef | grep WeChat | awk '{print $8}'");
} else if (Settings.isWindows()) {
cmd.add("cmd.exe /c wmic process where caption='WeChat.exe' get commandline /value");
} else {
cmd.add("/bin/sh");
cmd.add("-c");
cmd.add("ps -ef | grep WeChat");
}

上面例子中,先不要看win下的命令,mac下执行程序为/bin/sh, 选项为-c, 参数为ps -ef | grep WeChat | awk '{print $8}'

Windows中命令中,其他命令和Unix中类似,除了wmic。比如cmd.exe /c wmic process where caption='WeChat.exe' get commandline /value这条命了,在Windows的命令窗口是可以执行,但是Java中执行报错或者无结果。

具体为什么会这样,我目前不清楚。不过,在stackover flow中找到了一个解决办法,在打开命令窗口之后,用输入流的方式将命令出入到命令窗口。具体看我这边写的一个方法,里面if (cmd.contains("wmic")) {条件中的内容则为处理wmic特殊命令的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static List<String> execWinCmd(String cmd) {
if (StringUtils.isEmpty(cmd)) {
return Collections.emptyList();
}
Runtime runtime = Runtime.getRuntime();
List<String> result = new ArrayList<>();
BufferedReader br = null;
String message;
try {
if (cmd.contains("wmic")) {
cmd = cmd.substring(cmd.indexOf("wmic") + 4);
Process process = runtime.exec("wmic.exe");
OutputStreamWriter out = new OutputStreamWriter(process.getOutputStream());
out.write(cmd);
out.flush();
out.close();
br = new BufferedReader(new InputStreamReader( process.getInputStream()));
} else {
Process process = runtime.exec(cmd);
br = new BufferedReader(new InputStreamReader(process.getInputStream()));
}
if (br != null) {
while ((message = br.readLine()) != null) {
result.add(message);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (br != null) {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return result;
}

文章写完了,感谢大家!如果大家有什么意见或者建议,请发Email:me@zhoujunwen.win。

注:Windows设置这一节的内容基本来自:《关闭远程桌面后如何使UI自动化程序仍处于可交互状态》,感谢作者:Flicker

参考文章:
关闭远程桌面后如何使UI自动化程序仍处于可交互状态
Sikulix官方文档
tessdata
使用Google开源tesseract OCR用语言库报allow_blob_division解决方案
windows中文文档