周一早上八点四十,你端着咖啡坐到工位,打开 SAP Logon,双击 PRD,等三秒,敲工号,Tab,敲密码,Tab,语言选 ZH,回车。如果手上还挂着 DEV、QAS 和一套老 ECC,这一整套动作要重复四遍。要是哪天手抖选错了客户端号,登录进去看到的是别的公司代码,还得退出来重来。MySapLogon 就是在这种"每天都要做、做一次烦一次"的场景下长出来的小工具——它把"选系统 → 输账号 → 输密码 → 选语言 → 回车"这五步压缩成一次双击,顺带把密码从明文里救出来。这篇是"流星程序集"的第十八篇,我把这个自动登录 SAP 系统的工具从原理、选型、代码到踩坑,完整地摊开讲一遍。
它适合两类人:一类是每天要在多个 SAP 系统之间来回切的业务顾问、财务、物料、开发同学;另一类是要做批量测试、批量截图、批量跑报表的自动化同学。如果你只是偶尔登一次 SAP,那这篇文章对你的收益有限;但只要你一周有三天以上要开两个以上的客户端,后面这些内容能省下的时间是很实在的。
1. 每天重复三次的登录动作,值得写一个 MySapLogon
1.1 手动登录的时间账和出错账
先算一笔账。一次完整的人工登录,从点击到进入初始屏幕,手快的人 12 秒,手慢的人 20 秒,取中位数 15 秒。一天按开 3 个客户端算,45 秒;一年 250 个工作日,大约是 3.1 小时。这个数字听起来不算惊人,但真正的成本不在时间上,而在出错率和注意力消耗上。
人工登录的错误模式非常固定:客户端号敲成 200 而不是 100;语言没注意,进了一个德语界面,然后对着"Benutzer"发懵;密码输到一半被同事喊走,回来按了回车结果报了三次失败,账号被锁;更常见的是——密码框不能复制粘贴,密码又要求大小写加数字加符号,于是很多人把密码设成了好记但不符合安全策略的弱口令,或者干脆写在便签纸上贴在显示器边框。第三种情况才是最要命的:为了效率,人会自动降低安全标准。
自动化登录解决的其实是两个问题:一个是"省动作",一个是"把密码从人的记忆和便签里挪到加密存储里"。第二个问题比第一个重要得多。很多人做自动登录工具,做着做着就变成了把明文密码写进 ini 文件,这等于把贴在显示器上的便签变成了贴在共享盘上的便签,风险反而放大了。
1.2 MySapLogon 真正要啃下的三块骨头
把这件事想清楚之后,MySapLogon 的设计目标就明确了,它必须同时解决三件事:
第一块是通道。SAP GUI 不是一个开放的窗口程序,它对外提供的自动化入口只有"SAP GUI Scripting"这一条官方路径,而这条路径在客户端和服务端各有开关,默认状态下大概率是关着的。
第二块是凭据。密码怎么存、存成什么格式、换台电脑还能不能用、换个人登录 Windows 还能不能用,这几个问题决定了这个工具是"能用"还是"敢用"。
第三块是登录之后的乱流。点完登录按钮不等于进了系统。多重登录提示、版权信息、系统公告、密码即将过期提醒,任何一个弹窗卡在那里,自动化就变成了"打开了登录框然后发呆"。
这三块骨头里,第一块是技术问题,第二块是安全设计问题,第三块是耐心问题。我见过很多同类工具,第一块做得漂漂亮亮,第二块直接躺平,第三块压根没考虑,结果就是演示的时候很好用,真用两周就没人开了。
1.3 先说清楚:哪几种情况别用自动化登录
在动手之前,有几个场景我要明确劝退,这不是技术做不到,而是做了不划算或者不合规。
共享账号不要用。如果一个账号是多人共用的(虽然 SAP 的授权模型本来就不该这么搞),把它加密存在某个人的电脑上,本质上是把共享凭据私有化了,一旦这个人离职或者换机器,谁都不知道密码是什么。这种账号的正确解法是走正式的 SSO 或账号重分配流程。
生产系统的敏感账号要谨慎。财务、人事、薪酬相关的账号,很多公司的内部审计是明确要求"登录动作必须有人工确认环节"的。这种账号做自动登录要先跟内控确认,别自己先做了再解释。
已经上了正式单点登录的环境不要重复造轮子。如果贵司已经通过 SNC 加 Kerberos 那一套做了企业级单点登录,登录环节本来就不需要敲密码,你再搞一个存密码的小工具,属于逆着架构走。
排除掉这几种,剩下的场景——开发机、测试机、培训机、个人专用的业务账号——就非常适合做自动化。下面讲的所有内容,都建立在"账号是你个人的、系统是可自动化访问的"这个前提上。
2. SAP GUI Scripting 是官方留的接口,但开关藏在两个地方
2.1 客户端开关:GUI 选项与注册表两条路
SAP GUI Scripting 的客户端开关在 SAP Logon / SAP GUI 的本地布局里。路径是:打开 SAP GUI 或 SAP Logon,点左上角那把小齿轮(定制本地布局),进"辅助功能与脚本",展开"脚本",勾上"启用脚本"。同一个页面上还有三个勾选项,分别是"脚本打开连接时通知""脚本连接到已存在的会话时通知""脚本重新连接时通知"。做自动化的时候,这三个全部要取消勾选,否则每次脚本一动手,屏幕上就弹一个确认框,自动化立刻变成半自动。
如果你要给十几台机器批量配置,一个个点太慢,可以走注册表。客户端脚本开关对应的位置在:
HKEY_CURRENT_USER\Software\SAP\SAPGUI Front\SAP Frontend Server\Security需要的值是:
| 值名称 | 类型 | 建议值 | 作用 |
|---|---|---|---|
| UserScripting | DWORD | 1 | 启用脚本 |
| WarnOnConnection | DWORD | 0 | 打开新连接时不弹提示 |
| WarnOnAttach | DWORD | 0 | 附加到已有会话时不弹提示 |
| WarnOnReconnect | DWORD | 0 | 重新连接时不弹提示 |
这四个值改完之后,必须重启 SAP GUI 客户端才生效。这一步特别容易被忽略,改完注册表直接跑脚本,然后怀疑是不是注册表路径写错了,实际上是老的进程还在用旧的配置。我踩过至少两次这个坑,都是在帮别人远程排查的时候,对方说"改了没用",我让他重启 GUI,问题就没了。
另外,有些企业会在HKEY_LOCAL_MACHINE\Software\SAP\SAPGUI Front\SAP Frontend Server\Security下做更高级别的策略锁定,管理员级别设成 0 之后,用户级别怎么改都会被覆盖回来。这种情况下你改 HKCU 是白费力气,得找 BASIS 或者桌面运维放开。
2.2 服务端开关:sapgui/user_scripting 与它的连带参数
客户端的门开了,服务端的门可能还关着。SAP 系统里有一个 Profile 参数叫sapgui/user_scripting,默认值是 FALSE。也就是说,即使你本地脚本环境全配好了,连接上去之后调用脚本接口,系统会直接拒绝。
这个参数分动态和静态两部分。动态部分可以用事务码 RZ11 直接改,改完立即生效,不用重启实例;静态部分需要改 Profile 文件再重启。做验证的时候先用 RZ11 临时打开最省事:
- 登录系统,输入事务码
RZ11。 - 参数名填
sapgui/user_scripting。 - 进入后点击"修改",把新值设为 TRUE,保存。
- 退出重新登录一次(这一步很关键,脚本权限是在会话建立时确定的)。
这里有个经验:RZ11 改的值在实例重启后会丢失。如果你只是自己做测试,没问题;如果是给团队用,一定要走正式的 Profile 变更流程,把这个参数写进实例的 Profile 文件里。否则某天夜里系统重启,第二天所有人的自动登录工具集体失效,你会被追着问"是不是你的程序坏了"。
同一个参数族里还有几个值得关注的:
sapgui/user_scripting_disable_recording:设成 TRUE 可以禁止录制脚本,有些安全要求高的企业会打开它。sapgui/user_scripting_force_notification:设成 TRUE 会强制弹出脚本通知,和客户端那三个勾选项是配合关系。sapgui/user_scripting_per_user:按用户粒度控制,比全局开关更精细。
如果你的环境里sapgui/user_scripting必须是 FALSE(比如生产系统的安全基线不允许),那我在 3.1 节讲的 sapshcut 命令行方案就是唯一的出路,因为那个方案走的是登录参数传递,不依赖脚本引擎。
2.3 用五行脚本先验证通道
在写整个工具之前,先用最短的代码验证通道通不通,这个习惯能帮你省下大量排查时间。找一台装好 SAP GUI 的机器,新建一个.vbs文件,内容如下:
Set obj = CreateObject("Sapgui.ScriptingCtrl.1") Set app = obj.GetScriptingEngine Set conn = app.OpenConnection("PRD", True) Set sess = conn.Children(0) WScript.Echo sess.Info.SystemName & " / " & sess.Info.Client & " / " & sess.Info.User双击运行。如果弹出一个窗口,显示PRD / 100 / 你的用户名,说明脚本通道完全打通,可以继续往下做。如果报错,对照下面这几种情况:
- 报
ActiveX 部件不能创建对象:客户端脚本开关没开,或者宿主进程位数和 SAP GUI 不匹配。 - 报
OpenConnection失败:"PRD"这个名字在你的 SAP Logon 里不存在,名字必须和登录面板上显示的连接名完全一致。 - 弹出脚本安全提示:
WarnOnConnection那三个值没设对。 - 能连上但
Info取不到值:服务端sapgui/user_scripting没开。
提示:
OpenConnection的第一个参数既可以是 SAP Logon 里那条连接的名字,也可以是一段完整连接串。用连接串的好处是它不依赖你本地的登录面板配置,换台机器也能跑。具体格式见 6.1 节。
3. 三条实现路线:sapshcut、Scripting、混合方案
3.1 路线一:sapshcut.exe 命令行直连
SAP GUI 安装目录里有一个不太引人注意的小程序叫sapshcut.exe,它接受一组命令行参数,直接拉起一个已登录的会话。典型命令长这样:
"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\sapshcut.exe" ^ -system=PRD -client=100 -user=JDOE -pw=YourPassword -language=ZH -maxgui几个参数的作用分别是:-system指定系统标识,-client指定客户端号,-user和-pw是账号密码,-language是登录语言,-maxgui表示最大化窗口。你还可以加-command=让它登录后直接跑某个事务码,比如-command=SE38。
这条路线的优点是极其简单,不依赖脚本引擎,服务端sapgui/user_scripting关着也能用,一行命令解决战斗。缺点是密码写在命令行里,同一台机器上任何能跑tasklist或者看进程命令行的人都能读到——Windows 的任务管理器默认不显示命令行,但 PowerShell 一条Get-CimInstance Win32_Process就全露出来了。
所以这条路线正确的用法是:只在内存里拼命令行,不要写进 bat 文件。由你的工具解密出密码,用进程调用的方式直接把参数传给 sapshcut,全程不落盘。
3.2 路线二:Scripting COM 填表登录
第二条路线就是 2.3 节验证过的那套,用脚本引擎模拟人工填表。完整一点的版本是这样:
Set obj = CreateObject("Sapgui.ScriptingCtrl.1") Set app = obj.GetScriptingEngine Set conn = app.OpenConnection("PRD", True) Set sess = conn.Children(0) sess.findById("wnd[0]/usr/txtRSYST-BNAME").Text = "JDOE" sess.findById("wnd[0]/usr/pwdRSYST-BCODE").Text = "YourPassword" sess.findById("wnd[0]/usr/txtRSYST-LANGU").Text = "ZH" sess.findById("wnd[0]").sendVKey 0这里有几个细节值得展开。txtRSYST-BNAME是用户名字段,pwdRSYST-BCODE是密码字段,txtRSYST-LANGU是语言字段,wnd[0]是主窗口。控件 ID 不是背下来的,是用 GUI 自带的"脚本录制和回放"功能录出来的。操作方式是:SAP GUI 定制本地布局 → 脚本 → 脚本录制和回放,打开录制,选一个存放目录,然后你在系统里正常操作一遍登录,停止录制,目录里就会生成一个.vbs,里面每一行都是准确的控件 ID。
这个功能是 SAP GUI Scripting 里最被低估的东西。很多人靠猜 ID 前缀(txt是文本框、ctxt是带搜索帮助的文本框、pwd是密码框、btn是按钮、rad是单选按钮)去拼,拼错了就报"找不到元素",然后浪费半小时。录制一遍,五秒钟解决。
这条路线的优点是精确、可控,能处理登录后的各种弹窗,能校验登录结果,还能顺带做后续的自动化操作。缺点是服务端必须开脚本,而且比 sapshcut 慢一点,因为要等窗口渲染。
3.3 路线三:混合方案与它的取舍理由
我在 MySapLogon 里最终用的是混合方案,逻辑是这样的:
先用 sapshcut 把会话拉起来,拿到一个已经填好账号密码的登录界面(实际上 sapshcut 会自动提交,所以是直接进入系统);然后如果服务端允许脚本,就用脚本引擎接管后续的弹窗处理和结果校验;如果服务端不允许脚本,就退化为纯 sapshcut 模式,只保证"开起来",不做后续校验。
为什么要这么绕?因为纯 sapshcut 有两个硬伤:一是它无法确认"到底登录成功了没有",如果密码错了,它照样拉起一个界面,然后停在那里报"用户名或密码错误",而你的工具以为一切正常;二是它处理不了登录后的弹窗。而纯 scripting 的问题是它依赖服务端参数,很多客户环境里这个参数是锁死的,你没法要求 BASIS 为了你一个小工具去改生产系统的 Profile。
混合方案的核心判断逻辑是:
On Error Resume Next Set app = CreateObject("Sapgui.ScriptingCtrl.1").GetScriptingEngine If Err.Number = 0 Then ' 脚本可用,走完整流程 Else ' 脚本不可用,退化为纯命令行拉起 End If On Error GoTo 0这个"先探测、再降级"的思路,是这类小工具能不能在真实企业环境里活下来的关键。永远不要假设运行环境和你开发机的环境一样,这是我做了十几个这类小工具之后最深的体会。
3.4 三路线对照表
| 维度 | sapshcut 命令行 | Scripting COM | 混合方案 |
|---|---|---|---|
| 服务端脚本参数要求 | 不要求 | 必须为 TRUE | 有则用,无则退化 |
| 密码暴露面 | 进程命令行可见 | 仅在内存与脚本变量中 | 取决于降级路径 |
| 登录结果校验 | 无 | 可精确校验 | 有脚本时可校验 |
| 弹窗处理能力 | 无 | 强 | 有脚本时强 |
| 实现复杂度 | 低 | 中 | 中高 |
| 适用场景 | 生产环境受限 | 开发测试环境 | 通用 |
这张表我贴在自己的工具文档里,每次有人问"为什么不用 XX 方案",直接甩表。
4. 密码不能明文躺硬盘:DPAPI 加密与配置结构设计
4.1 为什么 Base64 等于没加密
先做一个思想实验。你写了一个配置文件:
[PRD] user=JDOE password=Sk9ITjIwMjUh这个Sk9ITjIwMjUh看起来像乱码,但任何人把这段字符串丢进浏览器控制台跑一句atob("Sk9ITjIwMjUh"),一秒钟就还原成明文。Base64 是编码,不是加密,它的设计目标是把二进制数据塞进文本协议,从来就没打算保护任何东西。用 Base64 存密码,安全性等于零,只是让密码看起来不那么扎眼而已。
再往上一层是"自定义异或加密"或者"硬编码密钥的 AES"。这两个稍微好一点,但因为密钥就在程序里,任何人反编译一下 exe 或者用strings命令扫一遍文件,密钥就到手了。这类方案能挡住的是"同事随手打开你的配置文件看一眼",挡不住任何有意愿的人。
4.2 DPAPI:把密钥交给 Windows 账号
Windows 提供了一个叫 DPAPI 的接口,中文一般叫"数据保护 API",它能做到的事是:用当前 Windows 用户的登录凭据派生密钥来加密数据。这意味着两件事——第一,加密出来的密文只有同一个 Windows 用户能解开;第二,你不需要自己管理密钥,密钥由系统托管。
这个特性正好匹配我们的场景。同事拷走你的配置文件,在他的电脑上解不开,因为他不是同一个 Windows 用户;你把电脑重装系统换了账号,之前的密文也解不开,这是设计上的取舍,不是 bug。
C# 版本最省事,直接调ProtectedData:
using System.Security.Cryptography; using System.Text; public static string Protect(string plain) { byte[] raw = Encoding.UTF8.GetBytes(plain); byte[] enc = ProtectedData.Protect(raw, null, DataProtectionScope.CurrentUser); return Convert.ToBase64String(enc); } public static string Unprotect(string cipher) { byte[] enc = Convert.FromBase64String(cipher); byte[] raw = ProtectedData.Unprotect(enc, null, DataProtectionScope.CurrentUser); return Encoding.UTF8.GetString(raw); }C# 里DataProtectionScope有两个枚举值:CurrentUser和LocalMachine。一定要用CurrentUser,LocalMachine意味着这台机器上任何用户都能解密,安全性差一大截。
Python 版本需要pywin32:
import win32crypt blob = win32crypt.CryptProtectData(b"YourPassword", "MySapLogon", None, None, None, 0) with open("prd_pw.bin", "wb") as f: f.write(blob) with open("prd_pw.bin", "rb") as f: data = f.read() desc, plain = win32crypt.CryptUnprotectData(data, None, None, None, 0) print(plain.decode())PowerShell 版本更短,用ConvertFrom-SecureString就能实现同样效果,它底层也是 DPAPI:
$sec = ConvertTo-SecureString "YourPassword" -AsPlainText -Force $enc = ConvertFrom-SecureString $sec $enc | Out-File .\prd_pw.txt # 解密 $sec2 = Get-Content .\prd_pw.txt | ConvertTo-SecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec2) [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)三种语言任选,思路完全一致:加密一次,把密文写进配置文件,运行时解密,用完立刻把明文变量清掉。VBScript 里没有真正的内存擦除,但至少可以做到password = "",减少它存活的时间窗口。
4.3 配置文件长什么样
我的配置文件用的是 JSON,结构大致如下:
{ "version": 1, "systems": [ { "name": "PRD-生产", "conn": "/H/sap-prd.example.local/S/3200", "client": "100", "user": "JDOE", "lang": "ZH", "secret": "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA...", "autoCommand": "", "order": 1 }, { "name": "QAS-测试", "conn": "/H/sap-qas.example.local/S/3200", "client": "200", "user": "JDOE", "lang": "ZH", "secret": "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA...", "autoCommand": "SE38", "order": 2 } ] }几个设计说明。name是给人看的,用中文加系统类型,比纯 SID 好认;conn是给程序用的连接串,不依赖本地的 SAP Logon 面板;secret是 DPAPI 加密后的 Base64 密文,注意它和 Windows 账号绑定,所以这个字段不应该进 Git;order控制批量启动的顺序,我一般把最耗时的生产系统放最后启动,让它慢慢初始化。
还有一个细节:配置文件里不放密码,只放加密后的密文。首次配置时运行一个单独的MySapLogon --setup,交互式输入密码,程序调 DPAPI 加密后写回配置。这样即便配置文件被人看到,也拿不到任何东西。
5. 登录框点完只是开始:会打断自动化的几类弹窗
5.1 多重登录提示
最容易撞上的是多重登录提示。当同一个账号已经在系统里存在活动会话时,SAP 会弹一个对话框问你是"结束其他登录并继续"还是"取消本次登录"。这个弹窗的控件 ID 里包含radMULTI_LOGON_OPT这样的前缀,一般有两个单选项加一个确认按钮。
处理逻辑是:登录提交之后,不要立刻认为成功,而是轮询检查wnd[1]是否存在,存在且里面能通过findById找到usr/radMULTI_LOGON_OPT2这类控件,就选中它再按确认。
If sess.Children.Count > 1 Then Dim w1 Set w1 = sess.Children(1) If Not IsNull(w1) Then sess.findById("wnd[1]/usr/radMULTI_LOGON_OPT2").Select sess.findById("wnd[1]/tbar[0]/btn[0]").press End If End If我个人的选择是选中"继续"而不是"结束其他登录",因为结束其他会话可能会导致你正在跑的后台作业报表被中断。这个取舍在批量启动多个客户端的时候尤其重要——如果你第二台的自动化把第一台的会话踢掉了,工具看起来成功了,实际上你前面的工作全白做了。
5.2 版权信息与系统公告
SAP GUI 在首次连接某个版本的补丁包时会弹一次版权信息,点一下"继续"就没了,之后因为版本没变所以不会再弹。这个弹窗本身不麻烦,麻烦的是它会出现在第一次运行的时候,而你正好在测自动化,于是你以为是自己代码写坏了。
比它更烦的是系统公告。管理员可以用 SM02 往系统里发全局消息,用户一登录就弹出来。有些公司的公告是每天弹的,内容还是纯文本,自动化脚本如果不处理,每次登录都要卡在那。好消息是这类公告弹窗的按钮 ID 通常也是wnd[1]/tbar[0]/btn[0]或者wnd[1]/usr/btnSPOP-OPTION1,可以统一处理。
我的做法是写一个通用的"弹窗收割"函数,登录提交之后循环最多四次,每次看看有没有wnd[1],有就按类型处理,处理完再检查一次,直到没有弹窗为止。这个循环的上限一定要设,否则万一遇到一个点了按钮又弹出来的死循环,工具就挂在那了。
5.3 密码过期与首次登录改密
这类弹窗绝对不能自动点掉。SAP 的密码策略通常设置了密码有效期,快到期时系统会提示"密码将在 N 天后过期,是否现在修改"。如果你写了一个自动点"是"的逻辑,接着就是一个改密对话框,需要输入旧密码、新密码、确认新密码——你的脚本没有新密码,于是整个流程崩在半路,更糟的是可能因为多次尝试被锁定。
正确的处理方式是识别出来、停下来、告警。检测到这类弹窗(标题或控件里带密码相关字样)时,直接把当前会话的状态标记为"需要人工介入",弹一个系统托盘通知,然后退出自动化流程。用户看到通知,手动处理改密,下次用新密码重新跑一遍 setup。
首次登录改密是同理。新开账号或者管理员重置密码之后,SAP 会强制要求改密,这个环节必须人工完成。工具要做的是别去解析这个界面,因为它的控件结构在不同版本之间差异很大,硬编码容易出事。
5.4 弹窗处理的通用写法
把上面几种情况归纳一下,弹窗处理的结构大概是这样一个函数:
Function HandlePopups(sess) Dim i, wnd, txt HandlePopups = "OK" For i = 1 To 4 Set wnd = Nothing On Error Resume Next Set wnd = sess.findById("wnd[1]") On Error GoTo 0 If wnd Is Nothing Then Exit Function txt = wnd.Text If InStr(txt, "多个登录") > 0 Or InStr(txt, "Multiple") > 0 Then sess.findById("wnd[1]/usr/radMULTI_LOGON_OPT2").Select sess.findById("wnd[1]/tbar[0]/btn[0]").press ElseIf InStr(txt, "密码") > 0 And InStr(txt, "过期") > 0 Then HandlePopups = "NEED_MANUAL" Exit Function Else On Error Resume Next sess.findById("wnd[1]/tbar[0]/btn[0]").press On Error GoTo 0 End If Next End Function这段代码里有两个坑我想单独提一下。第一,Set wnd = Nothing这一句不能省,因为On Error Resume Next遇到 findById 失败时不会改变变量的值,如果你不主动清空,上一轮的 wnd 对象还在,下一轮判断Is Nothing就会误判。第二,wnd.Text取的是弹窗标题栏的文本,不同语言版本这个文本不一样,所以不要只匹配中文,最好中英文关键字都覆盖,或者干脆改用控件 ID 判断——ID 是稳定不变的,文本不是。
说到这里补一句:SAP 的很多界面文本会跟着登录语言变化,所以任何基于文本匹配的逻辑都是脆弱的。真正稳的做法是尽量用控件 ID 和控件类型来判断。
6. 多系统多客户端:连接参数从哪来、怎么管
6.1 从 SAPUILandscape.xml 与 saplogon.ini 里取参数
手敲连接串容易错,尤其是带消息服务器的系统,格式长这样:
/H/sap-prd.example.local/S/3200带消息服务器和登录组的格式更长:
/H/sap-prd.example.local/S/3600/M/sap-ms.example.local/S/3601/G/PUBLIC这两串东西不用自己拼,它们就躺在你本地的配置文件里。老版本 SAP GUI 用的是saplogon.ini,一般位于%APPDATA%\SAP\Common\目录下,里面每个系统一段,Conn=那一行后面跟的就是完整的连接串。新版本(7.60 之后)改用了SAPUILandscape.xml,同一个目录下,也有一份全局的在C:\ProgramData\SAP\SAPGUILandscape\下,里面用 XML 节点描述每个系统的<Server>、<SystemId>、<MessageServer>等信息。
MySapLogon 里我做了个"导入"功能:读取SAPUILandscape.xml,把里面所有系统列出来,让用户勾选要加进来的,然后自动填充conn、system字段,用户只需要补客户端号、账号和密码。这个功能看起来很小,但实际省事很多,因为手抄连接串特别容易把端口号抄错,而端口号抄错的报错信息非常含糊,排查起来很痛苦。
| SAP GUI 版本 | 本地配置文件名 | 典型路径 |
|---|---|---|
| 7.40 及更早 | saplogon.ini | %APPDATA%\SAP\Common\ |
| 7.50 过渡期 | 两者都有 | %APPDATA%\SAP\Common\ |
| 7.60 及之后 | SAPUILandscape.xml | %APPDATA%\SAP\Common\ 与 C:\ProgramData\SAP\SAPGUILandscape\ |
6.2 客户端号、语言、登录组的变体管理
同一个系统往往有多个变体。比如 PRD 系统里,100 是集团总部、200 是某个子公司;语言方面,有人习惯中文,有人习惯英文,做报表的时候英文界面反而更好认字段名;登录组方面,大型系统会配置多个登录组做负载分发。
我处理变体的方式是在配置里允许"一个系统多条条目",用name区分。比如:
PRD-总部-中文:client=100, lang=ZHPRD-总部-英文:client=100, lang=ENPRD-子公司-中文:client=200, lang=ZH
这样看起来配置项变多了,但用起来很直观,想开哪个双击哪个。比在工具里搞一堆下拉框去选客户端号和语言要简单得多,而且配置本身就是文本,复刻到另一台机器上只需要处理加密字段的不同。
唯一需要注意的是语言字段只影响登录界面和菜单文本,不影响数据本身。有人以为用 EN 登录看到的报表数字和 ZH 登录不一样,这是误解。但语言确实影响日期格式和数字的小数点/千分位显示,做数据比对的时候要留意。
6.3 批量启动时的顺序与并发
一键启动四个客户端的体验很爽,但有两点要控制。
第一是并发。同时拉起四个 SAP GUI 进程,每个进程启动要占用不少内存,老机器上可能会卡住一两秒,甚至触发内存告急。我的做法是串行启动,每个之间间隔 3 到 5 秒,等上一个的登录界面稳定了再起下一个。这个间隔不是随便定的,是测出来的:SAP GUI 从进程启动到登录界面可交互,冷启动大约 4 到 8 秒,热启动 2 到 3 秒。间隔设成 3 秒在大多数机器上够用,如果发现偶发失败,就往上调到 5 秒。
第二是顺序。生产系统通常最慢,因为要做各种权限检查和数据初始化,把它放最后启动可以和前面几个的启动时间重叠掉。反过来,如果你把生产放第一个,然后干等它启动完再启其他的,总时间反而更长。
串行启动的伪代码大概是这样:
foreach ($sys in $config.systems | Sort-Object order) { $pw = Unprotect-Dpapi $sys.secret Start-Process $sapshcut -ArgumentList @( "-system=$($sys.name)", "-client=$($sys.client)", "-user=$($sys.user)", "-pw=$pw", "-language=$($sys.lang)" ) $pw = $null Start-Sleep -Seconds 3 }注意$pw = $null这一句,用完立刻把明文变量清掉,这是好习惯。
7. 实机踩坑与排查清单
7.1 "CreateObject 失败" 的四种成因
这是最高频的报错,至少有四种完全不同的成因,需要分别排查。
第一种,客户端脚本开关没开。回到 SAP GUI 定制本地布局里的脚本页面看一眼,别忘了改完注册表要重启 GUI。
第二种,宿主进程位数不匹配。SAP GUI for Windows 长期以来是 32 位程序,它注册的 COM 组件也注册在 32 位视图下。如果你用 64 位的 PowerShell 或者 64 位的 Python 去CreateObject,可能找不到这个类。解决办法是把脚本宿主换成 32 位版本。PowerShell 有%SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe这个 32 位入口,Python 则要装 32 位解释器。
第三种,SAP GUI 没有作为 Automation 服务器注册。正常情况下安装 SAP GUI 时会注册,但如果用的是绿色版或者拷贝版,可能没注册。可以用regsvr32手动注册安装目录下的sapgui.ocx之类的组件,不过更稳妥的做法是重新跑一遍官方安装包。
第四种,用了GetObject("SAPGUI")但当前没有 GUI 实例在跑。GetObject是"附加到已有实例",如果 SAP GUI 压根没启动,它必然失败。要创建新实例应该用CreateObject("Sapgui.ScriptingCtrl.1")。这两个函数经常被混用,是新手最容易犯的错。
7.2 元素 ID 找不到与控件类型写错
排在第二位的坑是找不到元素 wnd[0]/usr/txtRSYST-BNAME这类报错。原因通常是三个。
第一个是窗口层次错了。wnd[0]是主窗口,如果有弹窗盖在上面,你要找的控件可能还在wnd[0]里,但输入焦点被wnd[1]抢走了,这时候应该先处理wnd[1]。反过来,如果你想找的控件本身就在弹窗里,那前缀就得写wnd[1]。
第二个是控件类型前缀写错。文本框是txt,带搜索帮助的文本框是ctxt,密码框是pwd,按钮是btn,单选按钮是rad,复选框是chk。我之前把ctxtRSYST-MANDT写成txtRSYST-MANDT,报错信息只说找不到元素,完全不提类型不对,硬是找了十分钟。记住一句话:客户端号字段是 ctxt 不是 txt。
第三个是界面还没加载完。脚本跑得比界面渲染快,尤其是冷启动的第一次登录。解决办法是加轮询等待,比如循环检查sess.findById("wnd[0]/usr/txtRSYST-BNAME")是否能取到,取到了再往下走,最多等 15 秒。不要用固定的WScript.Sleep 5000,那个在快机器上浪费四秒,在慢机器上又不够。
7.3 安全软件、输入法和多显示器的干扰
有几个干扰因素跟 SAP 没关系,但会影响自动化。
端点安全软件。很多企业的终端防护会拦截"程序模拟键盘输入"和"程序操控其他程序窗口"这类行为。如果你的脚本用的是脚本引擎内部接口(sendVKey、控件的Text赋值),一般不会被拦,因为它走的是 SAP 自己的 COM 接口。但如果你用SendKeys或者 AutoIt 的键盘模拟去做补充操作,被拦的概率就很高。能用 SAP 自己的接口就别用键盘模拟,这是原则。
中文输入法。输入法在密码框里可能会吞掉第一个字符,或者把符号变成全角。手工登录的时候人能看到,自动化的时候你只会收到"用户名或密码错误"。稳妥的做法是自动登录过程中不强依赖输入法状态,因为脚本引擎是直接给控件属性赋值,不经过输入法;但如果你的流程里有SendKeys,那就要先把输入法切到英文。
多显示器和缩放。这个问题只影响基于屏幕坐标的点击方案。尽量别用坐标点击,SAP GUI Scripting 提供的是控件级接口,不需要坐标准确。如果你的方案里有坐标,那大概率是绕路了,回头看看能不能用控件接口替代。
7.4 一张可以照着走的排查表
把上面所有坑整理成一张表,出问题的时候从上往下走:
| 现象 | 优先排查方向 | 快速验证方式 |
|---|---|---|
| CreateObject 失败 | 脚本开关 / 宿主位数 | 用 32 位宿主跑 2.3 节的五行脚本 |
| OpenConnection 失败 | 连接名或连接串错误 | 手动在 SAP Logon 里连一次同名连接 |
| 找不到元素 | 窗口层次 / 控件前缀 / 加载未完成 | 用脚本录制功能录一遍完整登录 |
| 登录后停在错误页 | 密码错误 / 账号锁定 | 手工登录验证账号状态 |
| 弹窗卡住流程 | 多重登录 / 公告 | 加弹窗收割循环,打日志记录每个弹窗标题 |
| 换台机器跑不通 | DPAPI 账号绑定 | 在新机器上重跑 setup 重新加密 |
| 偶发失败,重跑就好 | 等待时间不足 | 把固定 sleep 换成轮询等待 |
| 隔夜全部失效 | 实例重启导致 RZ11 动态参数丢失 | 检查 sapgui/user_scripting 是否走了 Profile |
最后再提一个我在实际维护里养成的习惯:让工具写日志。每次登录尝试记录时间、系统、客户端、耗时、结果、遇到的弹窗标题,日志文件按天切分。平时你不看,但一旦出问题,翻日志比回忆"当时屏幕上显示了什么"要靠谱得多。我遇到过一次案例,连续三天早上第一台客户端必失败,第二台正常,翻日志发现失败的那次总是多了一个 SM02 公告弹窗,而这个公告是每天早上七点五十五分由某个后台作业发出来的——如果我当时没日志,这个规律根本找不到。
关于 MySapLogon 这个工具本身,后续我打算再加两块东西:一是把配置里的secret换成本地 Windows 凭据管理器(Credential Manager)存储,走CredWrite/CredRead接口,多一层系统级管理;二是给每个系统加一个"登录后自动执行事务码"的开关,让它顺带把每天早上要跑的 MD07、KO88 检查也一起做了。等这两个做完,大概可以出第十九篇了。