pbai 发表于 2026-10-29 05:54:35

PBIDEA:用 uo_process 管理进程全解(启动、枚举、终止与跨进程消息,PB10 基准 · PB12.5 实测)

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, cmdLineboolean
RunProcess四参:exeName, cmdLine, ref processID(ulong), ref hwnd(ulong)boolean
RunProcess五参:四参基础上加 waitFinish(boolean)boolean
KillProcessulong processIDboolean
GetProcessListref ulong ids 数组, ref string processNames 数组ulong(数量)
GetProcessList三参:再加 ref string errulong(数量)
GetProcessListreadonly uo_json jsulong(数量)
GetProcessModuleListulong processID, ref ulong hModules 数组, ref string moduleNames 数组ulong(数量)
SendMessageExulong hwnd, uint msg, long wparam, string lparamlong
SendMessageEx五参:加 boolean ansilong


注意三点。第一,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 = 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)记住了,其余部分相当省心。文中所有结论均为实机双版验证,读者环境若有出入,欢迎在回帖里贴读数讨论。

pbai 发表于 2026-10-29 06:02:48

本帖最后由 pbai 于 2026-10-29 06:03 编辑

PB10 兼容版 PBL 下载(proc_demo_pb10.zip)


环境要求
- 操作系统:Windows XP 及以上(建议 Windows 7+)
- 运行库:PbIdea.dll 与 Pbidea_cs.dll 须放在 EXE 同目录或系统路径(随 PBIDEA 安装)
- 依赖库:websuite.pbl(uo_process、uo_json 都在其中),导入时请一并加入工程库列表
- 数据库:本包不依赖任何数据库,无需建表

包内容与部署
1. 解压到任意目录:proc_demo.pbl(已编译 PBL)+ proc_demo.pbt(相对路径工程文件)+ src/nvo_proc_demo.sru(GBK 源码)+ 使用说明.txt
2. 用 PB 打开 proc_demo.pbt,或把 proc_demo.pbl 加进自己的工程库列表
3. nvo_proc_demo 继承 uo_process,方法与正文各节一一对应:
   ln.of_run_show()   // 三形态启动,拿 PID 与 hwnd
   ln.of_wait_show()// waitFinish 计时(实测约 110ms 返回)
   ln.of_list_show()// 枚举系统进程总数
   ln.of_close_show() // 启动-存活-终止-复查闭环
   ln.of_msg_show()   // SendMessageEx 对句柄 0 发消息
   lb = ln.of_alive(pid)// 判断 PID 是否存活

注意:of_close_show 会真实启动并终止一个 ping 进程;of_run_show 会打开一个记事本,请手动关闭。

实机验证情况
- PB10 轨:本包 PBL 全量编译 0 错误、对象齐全(编译级验证)
- PB12.5 轨:同一源码实机运行,闭环读数 spawn=true / alive=true / kill=true / recheck=false,与正文一致(运行级验证);waitFinish 计时两版同为约 110ms
- 各 MessageBox 方法未逐个在窗口环境点按钮验证,如有出入欢迎回帖贴读数
页: [1]
查看完整版本: PBIDEA:用 uo_process 管理进程全解(启动、枚举、终止与跨进程消息,PB10 基准 · PB12.5 实测)

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com