马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_process 管理进程全解(启动、枚举、终止与跨进程消息,PB10 基准 · PB12.5 实测)
阅读说明
- 适用版本:PB10 基准 · PB12.5 实测(两版读数逐条一致);uo_process 是 PBIDEA(PbIdea 组件)提供的非可视对象,PB9 及以下不可用。
- 支持数据库:本文不涉及数据库,无需连接任何 DBMS。
- 操作系统与环境:Windows 7 及以上;运行前需保证 PbIdea.dll 与 Pbidea_cs.dll 在程序目录或系统路径中(随 PBIDEA 安装)。示例会启动 cmd/ping 并终止它,无需管理员权限。
- 难度系数:★★☆☆☆(API 少而直白,坑集中在参数语义)。
- 其它阅读说明:建议先读过 PBIDEA 系列总目录中 uo_json 一文,本文第六节会用到它的 JSON 重载。
一、为什么 PB 里管进程这么费劲
PowerBuilder 自带的 Run() 能把一个外部程序拉起来,但它有个先天缺陷:拿不到进程 ID。程序跑起来之后,你既不知道它是否还活着,也没有任何正规手段把它关掉——很多同事只能靠外部函数 Declare 去调 OpenProcess 和 TerminateProcess,签名一长串,还容易在参数类型上翻车。
PBIDEA 的 uo_process 把这一整套事情收进了 10 个函数签名里:启动进程(顺带把 PID 和主窗口句柄交给你)、枚举全系统进程、枚举某个进程加载的模块、按 PID 终止进程,还有一条跨进程发字符串消息的通道。这篇文章把每个函数的实测行为讲清楚——包括几个与直觉不符的地方,比如那个看着很像「等程序跑完」的 waitFinish 参数,实测它根本等不住。
二、对象全貌与 API 清单
uo_process 是普通非可视对象,所有能力都是通过 type prototypes 声明的 PbIdea.dll 外部函数。先 create 再经实例调用即可,全部签名如下:
| 函数 | 签名要点 | 返回 | | RunProcess | 两参:exeName, cmdLine | boolean | | RunProcess | 四参:exeName, cmdLine, ref processID(ulong), ref hwnd(ulong) | boolean | | RunProcess | 五参:四参基础上加 waitFinish(boolean) | boolean | | KillProcess | ulong processID | boolean | | GetProcessList | ref ulong ids 数组, ref string processNames 数组 | ulong(数量) | | GetProcessList | 三参:再加 ref string err | ulong(数量) | | GetProcessList | readonly uo_json js | ulong(数量) | | GetProcessModuleList | ulong processID, ref ulong hModules 数组, ref string moduleNames 数组 | ulong(数量) | | SendMessageEx | ulong hwnd, uint msg, long wparam, string lparam | long | | SendMessageEx | 五参:加 boolean ansi | long |
注意三点。第一,exeName 和 cmdLine 是分开的两个参数,不是拼好一整个命令行。第二,数组出参要预先声明成动态数组,函数内部自己扩容。第三,返回数量的两个函数,数量类型是 ulong,后面循环遍历时有个有符号无符号混用的编译坑,第八节细说。
三、RunProcess 三形态:启动并拿到 PID
最小启动只需两个参数。实测三种 exeName 写法:
- uo_process lp
- boolean lb1, lb2, lb3
- lp = create uo_process
- lb1 = lp.RunProcess('cmd.exe', '/c exit')
- lb2 = lp.RunProcess('C:\Windows\System32\cmd.exe', '/c exit')
- lb3 = lp.RunProcess('nosuch_p81.exe', '')
- destroy lp
- MessageBox('RunProcess 两参形态', '裸名 cmd.exe = ' + string(lb1) + '~n' + &
- '全路径 cmd.exe = ' + string(lb2) + '~n' + &
- '不存在的程序 = ' + string(lb3))
复制代码
PB12.5 与 PB10 实测读数完全一致:裸名 true、全路径 true、不存在的程序 false。也就是说 exeName 走系统 PATH 搜索,失败时只返回 false,不抛错也不弹窗,判成败只能看返回值。
要拿 PID,用四参形态。实测 pid 与 hwnd 都会带回来,hwnd 是新进程主窗口的句柄(cmd 这类控制台程序拿到的值非 0):
- // 输入值在前段统一定义:要启动的程序与参数
- string ls_exe = 'notepad.exe' // 要启动的进程
- string ls_arg = '' // 命令行参数,可为空串
- uo_process lp
- ulong lu_pid, lu_hwnd
- boolean lb_ok
- lp = create uo_process
- lb_ok = lp.RunProcess(ls_exe, ls_arg, lu_pid, lu_hwnd)
- destroy lp
- MessageBox('四参形态', 'ok=' + string(lb_ok) + ' pid=' + string(lu_pid) + &
- ' hwnd=' + string(lu_hwnd))
复制代码
两版实测均返回 true,pid 是真实系统 PID(把任务管理器打开按 PID 排序能对上),hwnd 非 0。
四、waitFinish 的真相:等不住
五参形态多了个 waitFinish,字面意思「等它跑完再返回」。我特意用 ping 做了计时对拍:ping -n 2 正常要约 1 秒,ping -n 5 要约 4 秒,如果 waitFinish 真等,cpu() 计时应分别是 1000 与 4000 上下。实测两版读数:两种时长都只等了约 110 毫秒就返回 true。
- uo_process lp
- ulong lu_pid, lu_hwnd
- long ll_c1, ll_ms
- boolean lb
- lp = create uo_process
- ll_c1 = cpu()
- lb = lp.RunProcess('cmd.exe', '/c ping -n 5 127.0.0.1 > nul', &
- lu_pid, lu_hwnd, true)
- ll_ms = cpu() - ll_c1
- destroy lp
- MessageBox('waitFinish 计时', 'ok=' + string(lb) + ' 耗时=' + string(ll_ms) + 'ms')
复制代码
结论要写进你的设计前提:waitFinish=true 只代表「进程创建成功」,不代表子进程已结束。需要真正等待时,用第八节的枚举法自己轮询进程是否消失,或者干脆让被启动程序结束时自己写标记文件。
五、GetProcessList:把全系统进程装进数组
两参数形态把 PID 数组和名字数组一次性带回来,返回值是数量。实测本机 PB12.5 轨 191 个进程、PB10 轨 198 个(两次运行进程数本来就不同),第一个是 System,名字带全路径,例如 bash.exe 的值是完整绝对路径而非裸文件名:
- uo_process lp
- ulong lu_ids[]
- string ls_names[]
- ulong lu_cnt
- long ll_cnt
- lp = create uo_process
- lu_cnt = lp.GetProcessList(lu_ids, ls_names)
- ll_cnt = lu_cnt
- destroy lp
- MessageBox('进程总数', string(ll_cnt))
复制代码
三参形态多一个 err 出参。实测成功时 err 被填成 success(长度 7),也就是说它不是「错误时才有值」的出参,而是状态描述:别用 Len(err)=0 判成败,用返回数量。
uo_json 重载则把整个清单打包成 JSON,实测结构是 process 数组,每个元素两个字段:id 是数字 PID,name 是全路径名。全文序列化约 33KB(本机 191 个进程时):
- uo_process lp
- uo_json lj
- ulong lu_cnt
- lp = create uo_process
- lj = create uo_json
- lu_cnt = lp.GetProcessList(lj)
- MessageBox('JSON 形态', '数量=' + string(lu_cnt) + ' 长度=' + string(Len(lj.Tostring())))
- destroy lj
- destroy lp
复制代码
需要检索、排序、过滤进程清单时,JSON 形态配合 uo_json 的取值方法比两个平行数组好用得多。
六、判断进程还活着:自己写一个 of_alive
uo_process 没有「按 PID 查存活」的直接函数,自己封一个最稳:枚举一遍找 PID。这里有一个实测踩到的编译坑——返回数量的类型是 ulong,直接拿它做 for 循环上界(for i = 1 to lu_cnt 这种写法)会报 C0110「Mixing signed and unsigned values in for statement」。PB12.5 报警告,而 PB10 的工具链连警告都会让对象导入落空,所以先赋给 long 变量再进循环,两版通吃:
- // n_uo 中判断 PID 是否存活;复制到任意继承或包含 uo_process 的对象里即可
- public function boolean of_alive (long pid);
- uo_process lp
- ulong lu_ids[]
- string ls_names[]
- ulong lu_cnt
- long i, ll_cnt
- boolean lb_f = false
- lp = create uo_process
- lu_cnt = lp.GetProcessList(lu_ids, ls_names)
- ll_cnt = lu_cnt
- for i = 1 to ll_cnt
- if lu_ids[i] = pid then
- lb_f = true
- exit
- end if
- end for
- destroy lp
- return lb_f
- end function
复制代码
七、KillProcess 与完整闭环
KillProcess 只收一个 ulong PID。把三、六节连起来就是一个完整闭环,实测两版读数逐条一致:启动 ping 后 alive=true,KillProcess 返回 true,睡一秒再查 alive=false:
- // 输入值前段定义:目标程序与参数
- string ls_exe = 'cmd.exe' // 启动一个长驻 ping
- string ls_arg = '/c ping -n 90 127.0.0.1 > nul'
- uo_process lp
- ulong lu_pid, lu_hwnd
- long ll_pid
- boolean lb1, lb2, lb3, lb4
- lp = create uo_process
- lb1 = lp.RunProcess(ls_exe, ls_arg, lu_pid, lu_hwnd)
- ll_pid = lu_pid
- lb2 = of_alive(ll_pid) // 预期 true
- lb3 = lp.KillProcess(ll_pid) // 预期 true
- sleep(1) // sleep 的单位是秒
- lb4 = of_alive(ll_pid) // 预期 false
- destroy lp
- MessageBox('闭环', '启动=' + string(lb1) + ' 存活=' + string(lb2) + &
- ' 杀掉=' + string(lb3) + ' 复查=' + string(lb4))
复制代码
两个实务提醒:杀掉的进程不会进回收站也没有确认弹窗,主窗口瞬间消失;如果目标进程有未保存数据,不会有任何阻止。另外杀别人要有权限,同一个桌面会话里的普通进程都杀得动,系统级受保护进程会返回 false。
八、GetProcessModuleList:进程里装了哪些模块
传入 PID,把模块句柄数组和模块名数组带回来。实测一个新启动的 cmd/ping 进程只返回 4 个模块,第一个是 KERNELBASE.dll,最后是 ntdll.dll——注意可执行文件本身不在列表里,这一点和任务管理器里看到的模块清单不完全一样,写依赖判断时别按「第一个是主程序」来假设。进程被杀掉之后再枚举同一个 PID,返回数量 0,不报错:
- uo_process lp
- ulong lu_mods[]
- string ls_modnames[]
- ulong lu_cnt
- long ll_cnt
- lp = create uo_process
- lu_cnt = lp.GetProcessModuleList(2468, lu_mods, ls_modnames)
- ll_cnt = lu_cnt
- destroy lp
- MessageBox('模块数', string(ll_cnt))
复制代码
示例里的 2468 换成你要查的真实 PID(比如第三节 RunProcess 拿到的那个)。
九、SendMessageEx:跨进程发一条字符串消息
这是 uo_process 里最特别的一个函数:给指定窗口句柄发消息,lparam 可以直接带字符串,组件内部替你处理跨进程内存分配——普通 SendMessage 只能传数值,想给别的程序传一段文本要自己 VirtualAllocEx 一整套。源码里给的典型场景是:A 程序用 FindWindow 找到 B 程序数据窗口的句柄,发「另存为」消息并直接指定文件名,避免普通消息传字符串时崩溃。五参形态多一个 ansi 参数,向 PB9 及以下的老程序发消息时传 true。
实测对无效句柄 0 发消息:两参形态与 ansi 形态都安静地返回 0,不崩不抛错。
- uo_process lp
- long lr1
- lp = create uo_process
- lr1 = lp.SendMessageEx(0, 16, 0, 'test text')
- destroy lp
- MessageBox('对句柄 0 发消息', '返回=' + string(lr1))
复制代码
给真实窗口发消息时,把 0 换成目标窗口句柄、16 换成目标程序约定的消息号即可。消息号是两个程序之间的私有协议,需要 B 程序里有人接。
十、与 Run() 的对比
| 能力 | Run() | uo_process | | 拿到 PID | 不能 | 四参/五参形态带回 | | 拿到主窗口句柄 | 不能 | 带回 | | 等待结束 | 不能 | waitFinish 不可靠,用枚举轮询 | | 终止进程 | 不能 | KillProcess | | 枚举系统进程 | 不能 | GetProcessList 三重载 | | 枚举进程模块 | 不能 | GetProcessModuleList | | 跨进程字符串消息 | 不能 | SendMessageEx |
一句话:涉及「启动之后还要管」的场景,直接用 uo_process;只是顺手开个文档看一眼,Run() 也够。
十一、错误形态清单
实测汇总,写代码前扫一眼能省不少时间:
- RunProcess 的 exeName 不存在:返回 false,无弹窗无异常。
- exeName 与 cmdLine 是分开的两参,cmdLine 以系统方式传给新进程,/c 之类开关属于 cmd.exe 的语法而非 RunProcess 的。
- waitFinish=true 实测约 110 毫秒即返回,不能当「等程序结束」用。
- GetProcessList 三参形态成功时 err=success,判成败看返回数量,别看 err 长度。
- 数量类型是 ulong,直接当 for 循环上界会报 C0110(PB10 工具链下警告即失败),先转 long。
- KillProcess 杀受保护系统进程返回 false;杀普通进程无确认、无撤销。
- GetProcessModuleList 的结果不含可执行文件本身,且数量随进程状态浮动,别拿它做安全审计。
- SendMessageEx 对无效句柄返回 0;消息号是收发双方私有协议。
- 整个对象失败都不抛错,成败只能盯返回值。
- uo_process 依赖 PbIdea.dll 运行库,部署时两个 DLL(PbIdea.dll、Pbidea_cs.dll)必须随程序分发。
十二、PB12.5 差异
本轮全部探针在 PB10 与 PB12.5 各跑一遍,除进程总数、PID、hwnd 这类随环境变化的值外,所有行为读数逐条一致:三形态返回值、错误形态、waitFinish 计时、枚举数量与结构、闭环生死判定、模块枚举、SendMessageEx 返回值,全部相同。可以按 PB10 语法放心写,高版本行为不变。
十三、小结
uo_process 一共十来个签名,撑起了 PB 缺失多年的进程管理三件套:启动可拿句柄、随时可查生死、一句话可终止。把第三、六、七节的三个片段拼起来,就是一个能直接用的「启动-守护-回收」小框架;第六节的坑(C0110)记住了,其余部分相当省心。文中所有结论均为实机双版验证,读者环境若有出入,欢迎在回帖里贴读数讨论。 |