Windows系统软件自动化程序不能在mstsc远程断开的时候正常运行的解决方案
- 1 背景
- 2 问题所在
- 3 解决问题过程 (不喜欢看我BB可以直接跳过看第4点 )
- 3.1 联系微软客服得到的解决办法
- 3.2 出现了参数错误提示
- 3.3 结合自己的尝试后得到最终解决办法
- 4 使用console模式的方法总结
- 4.1 使用 query 命令查看SessionID
- 4.2 使用 tscon 命令切换console模式
- 5 升级解决方案(脚本)
1 背景
因为有个项目需要在一直运行一个exe的第三方软件以便于我们项目提供物质基础,所以决定在云服务器上购买一个Windows Server一直开着机运行。并实现自动化点击软件上的按钮个一些输入。 所以我需要使用按键精灵或者pywinauto等方式实现。因为考虑到我之后需要通过API接口方式去调用这个软件,所以我选了Python(Flask+PyWinAuto)的形式。前面一切都通顺利的,但是后面发现我的在程序正常运行的情况下,只要我一旦关闭远程连接,程序就会报错(报错内容为 当前没有激活的窗口),我的模拟鼠标点击和键盘输入还有截屏功能全部失效。后面经过查找资料发现相关资料太少所以联系微软的客服得到了解决方案,最终解决,并记录在此。
2 问题所在
使用一些自动化工具(如 按键精灵 、win32api、PyWinAuto…… )等在远程的Windows服务器上运行时候一旦远程窗口关闭,模拟鼠标点击和键盘输入还有截屏等功能很有可能失效。失效的的原因是当前连接以及不活动了,在我们使用mstsc进行远程连接的时候,实际上我们使用的RPC的形式进行连接。RPC一旦断开Windows就会判断为用户登陆连接断开。从而导致程序认为没有了活动的窗口,但实际上程序完全可以在我们断开连接之后正常运行的。办法就是让服务器的连接一直保持活跃状态。Windows上使用console模式进行连接的方式,当远程连接断开的时候可以保持一直活跃的状态。
3 解决问题过程 (不喜欢看我BB可以直接跳过看第4点 )
3.1 联系微软客服得到的解决办法
我在微软官网上找到了客服,客服也致电我告诉我办法,并发邮件图文告诉我步骤。 微软的客服服务态度也挺不错。
3.2 出现了参数错误提示
我是直接使用 邮箱中的方法 输入tscon命令后提示 参数错误。
3.3 结合自己的尝试后得到最终解决办法
后来直接看提示,无意中尝试了一下 密码改成 * 成了!
4 使用console模式的方法总结
4.1 使用 query 命令查看SessionID
以管理员方式打开CMD或者PowerShell(很基本的东西不会怎样打开自行百度)。使用命令 query session 或者query user 查询当前用户的session ID。注意:每次远程连接session ID是可能改变的,一定要每次都查询一下
query session
# 或者
query user
下图为使用 query session运行结果,用户名已做打码,可以看到前面带有>
的是当前用户(图中涂成黄色的)红色箭头指向的就是Session ID。 使用query user的表头顺序会改变,但是现实内容差不多。
4.2 使用 tscon 命令切换console模式
可以尝试使用 tscon [session ID] /password:[user password] /dest:console
的形式操作,但是为了万无一失。我们使用先不输入密码的方式,命令如下(这里的2对应上面查到的session ID):
tscon 2 /password:* /dest:console
然后会提示你要输入密码,输入你的Windows用户密码即可,此时远程会关闭。自动化程序会在后台正常运行。注意:每次远程都会重新切换回RDP模式,所以每次远程之后都必须使用这个方式退出远程在可以,不能直接关闭远程连接。
5 升级解决方案(脚本)
因为每次断开连接都需要输入两行代码。对于我来说还好,因为我是程序员。但是对于一些例如不会代码的产品经理等就很不友好,他们可能只会远程电脑。如果他们远程了服务器不会使用切换console模式直接把窗口关了,而我又不知道,导致程序不能正常运行,那可不是GG。所以我决定写个Python脚本并使用PyInstaller打包成exe,叫他们每次远程看了Windows服务器之后必须只能通过这个双击这个程序关闭远程。
我在这里就提供我的脚本的代码,但是打包成的exe程序我就要不提供了,没有使用第三方库打包起来很简单,论坛上也有很多关于PyInsatller把Python程序打包exe的教程。或者多人需要的话留言告诉我把脚本代码再写通用化一点打包一个发出来。
代码思路和解析都写在注释了,我比较懒就不一一讲解了。
import re
import getpass
import subprocess
import time
def getSessionID() -> int:
"""
获取当前用户的sessionID
:return: 当前用户的sessionID # type:int
"""
# 1 获取当前Windows用户的用户名
win_user_id = getpass.getuser()
# 2 运行命令 query session 命令
res = subprocess.Popen("query session", shell=True, stdout=subprocess.PIPE)
# 3 拿到命令的输出(输出的内容需要用.decode('gbk')转换字符类型)
''' 输出Demo:
会话名 用户名 ID 状态 类型 设备
services 0 断开
>console User1 5 运行中
7a78855482a04... 65536 侦听
rdp-tcp 65537 侦听
'''
res_str_list = [i.decode('gbk') for i in res.stdout.readlines()]
# 4 根据 本机会话前面是有一个 > 取到本机会话哪一行数据
this_session = [i.split() for i in res_str_list if i[0] == ">"]
if not this_session: # 如果拿不到当前会话
raise Exception("The session cannot be obtained because the '>' cannot be matched.")
# 5 通过正则表达式提取ID (BTW: 希望你的Windows用户名不要那么长,不然可能会变成省略号,这里就不好提取了)
match_str_list = re.findall("{}\d+".format(win_user_id), "".join(this_session[0]))
if not match_str_list: # 如果拿不到当前会话
raise Exception("The session cannot be obtained because the username: {} cannot be matched.".format(win_user_id))
return int(match_str_list[0].replace(win_user_id, ""))
def runSwitchConsoleModeCMD(session_id, password:str):
"""
运行切换console session命令
:param session_id: 当前用户的sessionID
:param password: 当前用户的密码
"""
session_id = int(session_id) # 转成int类型,主要是为了不是int或者不使int的字符串报错
# 1 运行tscon命令切换console模式 # 此处已修改感谢评论区额的小伙伴
res = subprocess.Popen("tscon {} /password:* /dest:console".format(session_id), shell=True,
stdout=subprocess.PIPE, stdin=subprocess.PIPE)
# 2 等一定时间然后输入密码
time.sleep(2)
res.stdin.write(password.encode('gbk'))
if __name__ == '__main__':
# 控制台输入密码
pwd = input("Enter this windows user's password: ")
# 写死密码
pwd = pwd or "XXXXXXXXXXXXXXXXXX"
try:
runSwitchConsoleModeCMD(getSessionID(), pwd)
except Exception as e:
print(e)
input("Press enter to exit...")
Windows系统软件自动化程序不能在mstsc远程断开的时候正常运行的解决方案
- 1 背景
- 2 问题所在
- 3 解决问题过程 (不喜欢看我BB可以直接跳过看第4点 )
- 3.1 联系微软客服得到的解决办法
- 3.2 出现了参数错误提示
- 3.3 结合自己的尝试后得到最终解决办法
- 4 使用console模式的方法总结
- 4.1 使用 query 命令查看SessionID
- 4.2 使用 tscon 命令切换console模式
- 5 升级解决方案(脚本)
1 背景
因为有个项目需要在一直运行一个exe的第三方软件以便于我们项目提供物质基础,所以决定在云服务器上购买一个Windows Server一直开着机运行。并实现自动化点击软件上的按钮个一些输入。 所以我需要使用按键精灵或者pywinauto等方式实现。因为考虑到我之后需要通过API接口方式去调用这个软件,所以我选了Python(Flask+PyWinAuto)的形式。前面一切都通顺利的,但是后面发现我的在程序正常运行的情况下,只要我一旦关闭远程连接,程序就会报错(报错内容为 当前没有激活的窗口),我的模拟鼠标点击和键盘输入还有截屏功能全部失效。后面经过查找资料发现相关资料太少所以联系微软的客服得到了解决方案,最终解决,并记录在此。
2 问题所在
使用一些自动化工具(如 按键精灵 、win32api、PyWinAuto…… )等在远程的Windows服务器上运行时候一旦远程窗口关闭,模拟鼠标点击和键盘输入还有截屏等功能很有可能失效。失效的的原因是当前连接以及不活动了,在我们使用mstsc进行远程连接的时候,实际上我们使用的RPC的形式进行连接。RPC一旦断开Windows就会判断为用户登陆连接断开。从而导致程序认为没有了活动的窗口,但实际上程序完全可以在我们断开连接之后正常运行的。办法就是让服务器的连接一直保持活跃状态。Windows上使用console模式进行连接的方式,当远程连接断开的时候可以保持一直活跃的状态。
3 解决问题过程 (不喜欢看我BB可以直接跳过看第4点 )
3.1 联系微软客服得到的解决办法
我在微软官网上找到了客服,客服也致电我告诉我办法,并发邮件图文告诉我步骤。 微软的客服服务态度也挺不错。
3.2 出现了参数错误提示
我是直接使用 邮箱中的方法 输入tscon命令后提示 参数错误。
3.3 结合自己的尝试后得到最终解决办法
后来直接看提示,无意中尝试了一下 密码改成 * 成了!
4 使用console模式的方法总结
4.1 使用 query 命令查看SessionID
以管理员方式打开CMD或者PowerShell(很基本的东西不会怎样打开自行百度)。使用命令 query session 或者query user 查询当前用户的session ID。注意:每次远程连接session ID是可能改变的,一定要每次都查询一下
query session
# 或者
query user
下图为使用 query session运行结果,用户名已做打码,可以看到前面带有>
的是当前用户(图中涂成黄色的)红色箭头指向的就是Session ID。 使用query user的表头顺序会改变,但是现实内容差不多。
4.2 使用 tscon 命令切换console模式
可以尝试使用 tscon [session ID] /password:[user password] /dest:console
的形式操作,但是为了万无一失。我们使用先不输入密码的方式,命令如下(这里的2对应上面查到的session ID):
tscon 2 /password:* /dest:console
然后会提示你要输入密码,输入你的Windows用户密码即可,此时远程会关闭。自动化程序会在后台正常运行。注意:每次远程都会重新切换回RDP模式,所以每次远程之后都必须使用这个方式退出远程在可以,不能直接关闭远程连接。
5 升级解决方案(脚本)
因为每次断开连接都需要输入两行代码。对于我来说还好,因为我是程序员。但是对于一些例如不会代码的产品经理等就很不友好,他们可能只会远程电脑。如果他们远程了服务器不会使用切换console模式直接把窗口关了,而我又不知道,导致程序不能正常运行,那可不是GG。所以我决定写个Python脚本并使用PyInstaller打包成exe,叫他们每次远程看了Windows服务器之后必须只能通过这个双击这个程序关闭远程。
我在这里就提供我的脚本的代码,但是打包成的exe程序我就要不提供了,没有使用第三方库打包起来很简单,论坛上也有很多关于PyInsatller把Python程序打包exe的教程。或者多人需要的话留言告诉我把脚本代码再写通用化一点打包一个发出来。
代码思路和解析都写在注释了,我比较懒就不一一讲解了。
import re
import getpass
import subprocess
import time
def getSessionID() -> int:
"""
获取当前用户的sessionID
:return: 当前用户的sessionID # type:int
"""
# 1 获取当前Windows用户的用户名
win_user_id = getpass.getuser()
# 2 运行命令 query session 命令
res = subprocess.Popen("query session", shell=True, stdout=subprocess.PIPE)
# 3 拿到命令的输出(输出的内容需要用.decode('gbk')转换字符类型)
''' 输出Demo:
会话名 用户名 ID 状态 类型 设备
services 0 断开
>console User1 5 运行中
7a78855482a04... 65536 侦听
rdp-tcp 65537 侦听
'''
res_str_list = [i.decode('gbk') for i in res.stdout.readlines()]
# 4 根据 本机会话前面是有一个 > 取到本机会话哪一行数据
this_session = [i.split() for i in res_str_list if i[0] == ">"]
if not this_session: # 如果拿不到当前会话
raise Exception("The session cannot be obtained because the '>' cannot be matched.")
# 5 通过正则表达式提取ID (BTW: 希望你的Windows用户名不要那么长,不然可能会变成省略号,这里就不好提取了)
match_str_list = re.findall("{}\d+".format(win_user_id), "".join(this_session[0]))
if not match_str_list: # 如果拿不到当前会话
raise Exception("The session cannot be obtained because the username: {} cannot be matched.".format(win_user_id))
return int(match_str_list[0].replace(win_user_id, ""))
def runSwitchConsoleModeCMD(session_id, password:str):
"""
运行切换console session命令
:param session_id: 当前用户的sessionID
:param password: 当前用户的密码
"""
session_id = int(session_id) # 转成int类型,主要是为了不是int或者不使int的字符串报错
# 1 运行tscon命令切换console模式 # 此处已修改感谢评论区额的小伙伴
res = subprocess.Popen("tscon {} /password:* /dest:console".format(session_id), shell=True,
stdout=subprocess.PIPE, stdin=subprocess.PIPE)
# 2 等一定时间然后输入密码
time.sleep(2)
res.stdin.write(password.encode('gbk'))
if __name__ == '__main__':
# 控制台输入密码
pwd = input("Enter this windows user's password: ")
# 写死密码
pwd = pwd or "XXXXXXXXXXXXXXXXXX"
try:
runSwitchConsoleModeCMD(getSessionID(), pwd)
except Exception as e:
print(e)
input("Press enter to exit...")