PowerBuilder 运行跟踪全解:Trace 函数族 8 个函数的实测行为、失败残留与 .trc 文件结构(PB10 基准 · PB12.5 实测)
PowerBuilder 运行跟踪全解:Trace 函数族 8 个函数的实测行为、失败残留与 .trc 文件结构(PB10 基准 · PB12.5 实测)阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文不涉及 PBIDEA 组件,全部是 PowerBuilder 自带能力)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows 7+;无需任何第三方 DLL,TraceOpen / TraceTree 都是 PB 运行库自带;落盘目录必须已存在(否则会踩第 4 节的坑)
4. 难度系数:★★★☆☆(8 个函数本身十分钟就能调通;难点全在「静默」二字上——不报错的坑有四个,其中两个会让进程直接消失)
5. 其它阅读说明:本文所有读数来自 PB10 与 PB12.5 双轨实机运行。凡是「实测」二字后面跟数字的,都是本机跑出来的原值;在本机没能测到的边界,会明确写「未实测」。本文还逐字节反向了 .trc 文件结构,凡是给偏移量的地方都标了样本文件与体积,你可以自行复算。
一、先说结论:Trace 能做什么、不能做什么
PowerBuilder 里有两条完全不同的「跟踪」路线,很多人把它们搞混了:
[*]Trace 函数族(本文主题):把 PB 运行时的活动写进一个 .trc 文件。8 个函数,纯 PowerScript,不需要任何附加组件。
[*]TraceTree / Profiling 对象:把已经生成的 .trc 读回来变成模型。这是第 12 节的内容。
先把边界划清楚,这决定了后面所有写法:
能力实机结果说明
TraceOpen / TraceClose✅ 跑通双轨都可用,返回 Success!
TraceBegin / TraceEnd✅ 跑通块标识可以为空串、可以为 null、可以 300 字符
TraceUser / TraceError✅ 跑通且真实落盘消息以 UTF-16LE 写入,中文完整保留
TraceEnableActivity / TraceDisableActivity✅ 真实生效开与关都能用体积差验证
10 个 ActXxx! 活动常量✅ 全部合法双轨 import 均 0 错
4 种 TimerKind✅ 全部可用是否带时间戳看体积差
是否只有 Enterprise 版能用❌ 不是本机 PB10 / PB12.5 全功能可用;EnterpriseOnlyFeature! 一次都没返回过
失败后再 TraceClose❌ 进程崩溃0xC0000005,TRY-CATCH 兜不住,见第 4 节
同一文件二次 TraceOpen⚠️ 截断上一份日志不报错,只是把上一次的内容清空重写,见第 6 节
把枚举返回值记进日志❌ 做不到long() 恒 0、string() 恒空串,见第 7 节
直接读 .trc 文本找自己的消息❌ 做不到UTF-16LE + 内含 NUL,String(blob) 会在第一个 NUL 处截断,见第 11 节
一句话概括这一族函数的性格:「成功的路径极其好走,失败的路径极其危险」。8 个函数里没有一个会在出错时抛异常;最严重的那一种,连异常都来不及抛。
二、签名全貌:8 个函数,以及帮助文档里写错的四处
官方帮助给出的完整签名如下(PowerScript Reference 卷,PB10 与 PB12.5 帮助一致):
函数签名参数类型
TraceOpenTraceOpen ( filename, timer )string + TimerKind
TraceCloseTraceClose ( )无
TraceBeginTraceBegin ( identifier )string
TraceEndTraceEnd ( )无
TraceEnableActivityTraceEnableActivity ( activity )活动常量
TraceDisableActivityTraceDisableActivity ( activity )活动常量
TraceUserTraceUser ( info, message )long + string
TraceErrorTraceError ( severity, message )long + string
八个函数全部返回 ErrorReturn。
顺带排掉一个坑:帮助库里还有一个 TraceFile = 'tracefile_name',那是 JDBC 驱动管理器跟踪的数据库参数(写进 SQLCA.DBParm 的),和本文这 8 个函数没有任何关系。搜「Trace」搜到它别顺手抄进代码。
帮助文档实测写错/写漏的四处
这四处不是「翻译问题」,是照抄就编译不过:
#帮助里的写法实测结果正确写法
1TraceOpen 的 timer 取值写 TimeNone!import 报 C0060 Illegal enumerated constant(双轨一致)TimerNone!
2TraceEnableActivity 示例里的 ActCreateObject! / ActDestroyObject!import 报 C0060(双轨一致)ActObjectCreate! / ActObjectDestroy!
3TraceUser 的示例写成单参 TraceUser("Starting database retrieval")import 报 C0052 Bad argument list for function: traceuser两参:TraceUser(100, "Starting…")
4把 TraceEnableActivity 的参数类型称作 ActivityKind声明成该类型报 C0001 Illegal data type;ActivityType 同样报 C0001(双轨一致)不声明类型,直接传 ActXxx! 常量
第 1 条的原文就写在 TraceOpen 的 arguments 里(TimeNone!- Do not log timer values),而同一个函数的示例代码里却写的是 TimerNone! —— 帮助自己和自己不一致,示例是对的。
第 4 条值得多说一句:ActivityKind 这个名字不能当参数类型用,但它的 10 个常量全部合法。所以正确姿势是把活动常量直接写进调用,例如 TraceEnableActivity(ActRoutine!),而不是绕一圈用一个 ActivityKind 形参去接。
附带一条编译器脾气:枚举类型名区分大小写。我最初把 ErrorReturn 写成全小写 errreturn,import 直接报 C0001 Illegal data type,改回原样即通过。
10 个活动常量与 4 个 timer 常量
TraceEnableActivity / TraceDisableActivity 接受的 10 个常量,双轨 import + 运行全部 Success!,一个不少:
ActUser!(默认开启)、ActRoutine!、ActESQL!、ActGarbageCollect!、ActObjectCreate!、ActObjectDestroy!、ActError!、ActLine!、ActProfile!、ActTrace!
TraceOpen 的 timer 参数,4 个常量全部可用:TimerNone!、Clock!、Process!、Thread!。
帮助说 ActLine! 开启会连带开启 ActRoutine!、ActRoutine! 关闭会连带关闭 ActLine!,且 ActProfile! / ActTrace! 是若干活动的缩写。这几条本文未单独实测,只做转述。
三、最小可用写法
把生命周期拆开看,就是四步,顺序不能乱:
ErrorReturn le
// 第一步:打开(文件此时已被创建/清空,但里面还没有内容)
le = TraceOpen('C:\trace\app_run.trc', TimerNone!)
IF le <> Success! THEN RETURN // ★ 失败就必须立刻收手,见第 4 节
// 第二步:开启想记录的活动(可多次调用;不调用则只有 ActUser!)
TraceEnableActivity(ActRoutine!)
TraceEnableActivity(ActError!)
// 第三步:打点
TraceBegin('main_block')
TraceUser(101, '程序启动完成')
TraceError(900, '配置读取失败,使用默认值')
TraceEnd()
// 第四步:关闭(内容在此时才真正写进磁盘)
le = TraceClose()
三个必须先建立的认知:
[*]TraceOpen 会立刻创建(或清空)目标文件,但此刻文件长度是 0。
[*]内容缓冲到 TraceClose 才落盘。实测:刚 TraceOpen 后 FileLength 为 0,TraceClose 之后变成 602 字节。
[*]每次 TraceOpen 都会把活动开关重置成「只有 ActUser!」,上一次开的活动不会保留。
第 2 条有个直接后果:如果你在程序运行中途去「偷看」这个 .trc,会看到一个 0 字节或残缺的文件——不是没写进去,是还没落盘。
另外,TraceBegin 打点前后用 TraceEnd 收口是成对的:TraceClose 之前没 TraceEnd 也能关,但块结构不完整,第 12 节回读时条数会和你预期不符。
四、★最重的一个坑:第一次 TraceOpen 失败之后,这个会话就废了
这是本文最需要你记住的一节,因为它同时具备三个最坏的性质:不抛异常、污染全局、最终崩进程。
4.1 失败之后,「错误粘连」到所有后续调用
先看失败是怎么产生的。两个最普通的诱因,双轨实测:
诱因首次 TraceOpen 返回
目录不存在(nosuchdir_zzz\a.trc)FileOpenError!
文件名是空串 ''FileOpenError!
看起来平平无奇——就是一个错误返回值而已。但真正的麻烦在后面:从这一刻起,同一个会话里所有 Trace 调用都会返回那个错误,而不是各自的真实错误。
打点法实测序列(打点写到文件里,崩溃前进到哪一步一目了然):
B1 start
B2 bad=FileOpenError! ← 一次失败的 TraceOpen
B3 good_after_bad=FileAlreadyOpenError! ← 紧接着一次「本该成功」的调用
(之后进程消失,B4 从未写入)
注意 B3:第二次调用返回的是 FileAlreadyOpenError!,一个和事实毫不相干的错误。文档说这个返回值是「TraceOpen 被重复调用」,但你其实只调用过一次成功的都没有。这个返回值会掩盖真实故障原因,让人往完全错误的方向排查。
4.2 一旦在失败态下 TraceClose,进程直接崩
更严重的在这里。三个独立探针(每例一个独立进程),打点全部停在崩溃前最后一步:
探针场景打点最后一行退出码
A空文件名 Open 失败 → CloseA2 open_empty=FileOpenError!3221225477
B目录不存在 Open 失败 → 先做一次正常调用 → CloseB3 good_after_bad=FileAlreadyOpenError!3221225477
C目录不存在 Open 失败 → CloseC2 bad=FileOpenError!3221225477
退出码 3221225477 = 0xC0000005,就是 access violation。这不是 PB 抛的运行时错误,是进程级的崩溃:.pbtest 运行器的输出是「exited before writing a result」,你的应用如果是窗口程序,表现就是「窗口突然不见了」。
4.3 TRY-CATCH 兜不住
有人会想:那我用 TRY/CATCH 包起来不就行了。实测不行:
T1 in try ← 已进入 try 块
T2 bad=FileOpenError! ← 失败的 Open
(此后没有 T3、没有 T9,进程消失)
打点文件停在 T2,catch ( Throwable lt_ex ) 里的 T9 caught 一次都没被执行。原因很直白:TRY-CATCH 处理的是 PB 运行时错误(RuntimeError),而这里是 C 层的访问违例,控制权根本回不到 PowerScript。
4.4 结论与唯一可行的防御
既然「失败之后无法安全退出」,防御只能做在失败之前:
[*]TraceOpen 之前先把路径查干净:目录是否存在、文件名是否为空、是否可写。
[*]确认目标文件已被占用时先删掉(也顺带规避第 6 节的截断问题)。
[*]TraceOpen 返回值不等于 Success! 时,绝对不要再调用任何 Trace 函数——包括 TraceClose。让这一轮的跟踪放弃掉,比让进程消失好得多。
第 13 节的工具类把这三条固化成了 of_prepare / of_safe_open 两个方法。
未实测项:多实例场景(同一个会话里两个对象各自 Trace)是否也互相污染,本轮没有测。上面所有结论都基于「一个会话里只有一个 Trace 状态机」这个前提。
五、文件什么时候真的落盘
这一节只有两张读数表,但它们是第 6 节的前提。
观测点实测值
TraceOpen 之后立刻 FileExiststrue(文件已被创建)
TraceOpen 之后立刻 FileLength0
TraceClose 之后 FileExiststrue
TraceClose 之后 FileLength602
所以顺序是:Open 建空文件 → 全程写内存缓冲 → Close 一次性落盘。
还有一条容易误判的行为:
场景TraceBegin 返回值
TraceClose 之后继续 TraceBeginFileNotOpenError!
TraceClose 之后继续 TraceCloseFileNotOpenError!
注意这里没有崩。也就是说:成功的 TraceClose 把状态干净地复位了,「错误粘连」只发生在 TraceOpen 失败之后。区分这两条能省掉大量误诊。
六、★同一文件二次 TraceOpen 会截断上一份日志
这是四个「不报错的坑」里最隐蔽的一个。
我的第一版探针是这样写的:打开文件 → 写一条带中文的 TraceUser → 关闭 → 再打开同一个文件 → 再写一个块 → 关闭。然后去文件里找那条中文消息。结果是:
len1=640 ← 第一次关闭后的体积
len2=624 ← 第二次关闭后的体积(变小了!)
中文消息 → NOT FOUND
体积变小说明第二次 TraceOpen 不是追加,而是从 0 开始重写。第一份日志连同那条中文消息一起没了。
反过来验证也成立:不带第二次 TraceOpen 的干净探针,同一条中文消息完整落盘,位置在偏移 290:
中文消息ABC → utf-16-le @ 290 ✅ 命中
所以这不是编码问题,是覆盖问题。第 4 节里 of_safe_open 先 FileDelete 再 TraceOpen 的做法,也正是为了让「文件已存在」这件事变得确定。
推论(未实测):如果需要在一次程序运行里保留多份跟踪,正确做法是用不同的文件名,而不是反复打开同一个文件。反过来,如果你想追加到同一份日志,这一族函数没有提供追加语义。
七、ErrorReturn 的真面目:8 个常量 long() 全 0、string() 全空
写日志的时候你多半会想记下「这一步返回了什么错误」。这里有个必须知道的结论:
// 实测:全部为 0
Long(Success!) // 0
Long(FileNotOpenError!) // 0
Long(FileOpenError!) // 0
Long(FileAlreadyOpenError!)// 0
Long(TraceStartedError!) // 0
// 实测:全部为空串
String(Success!) // ''
String(FileOpenError!) // ''
也就是说 ErrorReturn 是纯比较用的枚举:只能拿 = 去比常量,不能转成数字,也不能转成字符串。想要可读的错误名,只能自己写一层翻译(第 13 节的 of_lab)。
还有一条与帮助不符的实测:TraceError 并不是「永远返回 Success!」。
条件TraceError(999,'ERRMARK') 返回值文件里有没有 ERRMARK
未 TraceEnableActivity(ActError!)不是 Success!(落在我 8 个常量之外的取值上)没有
已 TraceEnableActivity(ActError!)Success!有
体积也对得上:同一段代码,不启用 ActError! 是 620 字节,启用后 644 字节,差 24 字节正好是一条 TraceError 记录。
关于那个「8 个常量之外的取值」:我用 Long() 取不出来(恒 0),所以无法断定它等于哪个具体整数。这一条只作为「实测现象」记录:ActError! 没开时,TraceError 的返回值不等于 Success!。 实践建议是:判成功就用 = Success!,别写 <> Success! 之外的假设。
八、活动开关:默认只有 ActUser!,开与关都真实生效
TraceOpen 会把活动重置为「只有 ActUser!」。这一点可以直接用体积差证明——2×2 对照,四个探针除「是否启用 ActRoutine!」和「是否真的干活(创建/销毁一个非可视对象)」外完全相同:
探针启用 ActRoutine!真的干活.trc 体积(PB10 / PB12.5)
v✗✗610 / 614
w✗✓648 / 650
x✓✓790 / 788
y✓✗670 / 668
读法:不启用时,干活与不干活差 38 字节;启用后差 120 字节(790−670)。启用让「干活」这件事被记录得更细,而不启用的那点增量只是块标识本身的字节数。
TraceDisableActivity 同样真实生效,而且是即时的:
场景体积
启用 ActRoutine! → 干活902
启用后立刻 TraceDisableActivity(ActRoutine!) → 同样干活652
差 250 字节,说明关掉之后确实不再记录了。
一个实用推论:只想记录自己打的点、不想被运行时活动淹没时,什么都别启用(保持只有 ActUser!),然后在关键位置 TraceUser 即可。这时 .trc 里几乎只有你自己的消息,体积可控。
九、TimerKind 四种与 .trc 头部的时间字段
四种 timer 全部可用,区别在于每条记录是否附带时间戳:
timer头部 0x0E0头部 0x0E8记录时间语义
TimerNone!01无时间字段
Clock!232墙钟(相对机器启动),微秒
Process!03本进程经过时间
Thread!04本线程经过时间
同一个「打 10 个点」的探针,TimerNone! 与其它三种的体积差是每记录 50 字节(10 个记录差 500 字节):每个记录多一个 8 字节时间戳,头部多 2 字节标志,正好凑成 50。
本机实测的一个 Clock! 时间戳是 339848.6536823 秒,约 3.93 天——和机器的连续运行时间是同一个数量级,这也印证了帮助里说的「相对于外部活动(如机器启动时间)」。
帮助里区分了 Clock!(墙钟,微秒)与 Process! / Thread!(进程/线程经过时间)。三种时间基数之间的换算关系本文未实测,只验证了「字段编码」与「体积差」这两件可证的事。
十、.trc 文件里到底存了什么
这一节是逐字节反向出来的,你可以拿任意一个 .trc 复算。
10.1 容器性质
.trc 是一个 UTF-16LE 文本容器,不是压缩包也不是自定义二进制结构。把整个文件按 UTF-16LE 解码,能直接读出:
[<app 名>][<库名>]
[<库>.grp][<库>.apl][<函数>.fun][<对象>.udo]
[<宿主可执行文件的绝对路径>]
(上面第二、三行已把本例的具体值做了占位替换;实际文件里就是你的应用名、库名、对象名,以及运行这个 PB 程序的 EXE 完整路径。)
结尾那一大段是符号表:库名、对象名、函数名,以及宿主可执行文件的绝对路径。开头几个字段:
偏移内容实测值
0x000magicPBTRACE(UTF-16LE,7 字符)
~0x032host 标识PB10 = INTEL 01 238;PB12.5 = INTEL 01 333
0x0C44 字节小端整数随内容增长(668 B 文件 = 330;1244 B 文件 = 904)
0x0C84 字节小端整数与文件尾相差 182 ~ 184 字节
0x0D84 字节小端整数TimerNone! 下为 0
0x0E04 字节小端整数timer 编码,见第 9 节
0x0E84 字节小端整数timer 序号,见第 9 节
host 那个字段是双轨差异中唯一稳定可复现的一条:我把 68 个 .trc 逐个扫描,34 个出自 PB10 的全部是 INTEL 01 238,34 个出自 PB12.5 的全部是 INTEL 01 333,零例外。
关于 app / lib 那两个名字:它们是运行这个 PB 程序自己的应用名与库名,不是 PB 固定值。本机运行宿主叫什么,这里就写什么;你程序里跑出来的会是你的应用名与库名。
关于 0x0C4 / 0x0C8:官方没有公开这两个字段的语义,上面只给观察值,不做推断。
10.2 记录格式
活动记录的外形是「一个 2 字节类型标记 + 长度前缀 + UTF-16LE 内容」。三种最常用的记录,双轨实测完全一致:
记录2 字节标记结构
TraceBegin09 0109 01 + 长度(4B) + 块标识(UTF-16LE)
TraceUser07 0307 03 + info(4B 小端) + 长度(4B) + 消息(UTF-16LE)
TraceError08 0308 03 + severity(4B 小端) + 长度(4B) + 消息(UTF-16LE)
符号表项09 0209 02 + 4B + 长度(4B) + 名字(UTF-16LE)
拿一个真实片段对照(TraceBegin('zblk1') + 两次 TraceUser):
010809 01 05 00 00 00 7A 00 62 00 6C 00 6B 00 31 00
└ 类型└ 长度=5 └ "zblk1"
011607 03 65 00 00 00 07 00 00 00 4D 00 41 00 52 00 ...
└ 类型└ info=101 └ 长度=7 └ "MARKAAA"
013007 03 34 12 00 00 07 00 00 00 4D 00 41 00 52 00 ...
└ 类型└ info=4660└ 长度=7 └ "MARKBBB"
65 00 00 00 就是 101,34 12 00 00 就是 4660。info 是按 4 字节小端原样写进去的整数,不是字符串,也没有任何转换。
TraceError 同理,喂 999 进去:
011608 03 E7 03 00 00 07 00 00 00 45 00 52 00 52 00 ...
└ 类型└ 999=0x3E7 └ 长度=7 └ "ERRMARK"
10.3 中文与超长标识
项目实测结果
TraceUser(7, '中文消息ABC')消息完整落盘为 UTF-16LE:中文消息ABC 命中于偏移 290
TraceBegin(300 个 'L')标识完整落盘不截断(300 个字符逐一定位到)
TraceBegin('') 空标识返回 Success!,块建立成功,只是没有标识文本
TraceBegin(null)(先 SetNull)返回 Success!,标识不落盘(与帮助说的「放一个空串」一致)
TraceUser(5, null)(message 为 null)返回 NULL,且不落盘(与帮助一致)
顺带纠一个我自己踩过的误判:最开始我在一个 .trc 里找中文消息找不到,差点写成「PB 会丢中文」。实际原因是那个探针二次 TraceOpen 了同一个文件(第 6 节),消息随第一份日志被截断掉了。换成单次 Open 的探针,中文一字不差。别急着怀疑编码,先确认自己没有覆盖文件。
10.4 嵌套 TraceBegin 与帮助不符
帮助明确写 TraceBegin 在已经调用过的情况下返回 TraceStartedError!。实测不是:
TraceBegin('n1') → Success! (标识 n1 落盘)
TraceBegin('n2') → Success! (★ 不是 TraceStartedError!)
而且更细的一层:n2 这个标识在文件里找不到。也就是说第二次 TraceBegin 既不报错、也不生效——它被静默忽略了。
顺序的多次 TraceBegin + TraceEnd(s1 一块、n1 一块)都正常,标识 s1、n1 都能定位到。
十一、TraceUser / TraceError 的落盘判据
既然消息真的写进去了,那么「怎么在代码里确认它写进去了」?
11.1 走不通的路:直接读文本
我原本的设想很简单:把 .trc 当 blob 读出来,按 UTF-16LE 解码,然后 Pos() 找自己的标记。实测走不通:
写法结果
String(lb, EncodingUTF16LE!)Len(lb)=668,但 Len(结果串)=7 —— 只得到 PBTRACE
String(lb)(不指定编码)同样 Len=7
原因是文件里紧跟 magic 之后就是 00 00,String(blob) 在第一个 NUL 处就截断了。
我还试过绕过去:把标记手工拼成「字符 + NUL」的交错模式再去 Pos。也不行——Char(0) 在本机返回空串(实测拼接后模式长度是 7 而不是 14),拼不出 NUL 字节。
11.2 走得通的三条判据
判据写法实测
体积FileLength(路径)开/关活动前后差 250 字节、差 24 字节;Close 前 0、Close 后 602
条目数TraceTree + EntryList(第 12 节)见下
时间戳选中带 timer 的 TimerKind 后体积按记录数线性增长10 记录差 500 字节
结论:要判定「记录有没有进去」,用体积与条目数;要读内容,用 TraceTree。不要试图直接解析 .trc 文本。
十二、把日志读回来:TraceTree 对象
TraceTree 是 PB 自带的系统类(双轨 create 成功,IsValid 为 true)。三步走:CREATE TraceTree → SetTraceFileName + BuildModel → EntryList 取条目。
TraceTree ltct
TraceTreeNode ltctn[]
ErrorReturn le1, le2
Integer li_n
ltct = CREATE TraceTree
IF NOT IsValid(ltct) THEN RETURN -1
le1 = ltct.SetTraceFileName('C:\trace\app_run.trc')
le2 = ltct.BuildModel()
IF le1 <> Success! OR le2 <> Success! THEN
DESTROY ltct
RETURN -1
END IF
ltct.EntryList(ltctn) // 顶层条目写进 ltctn 数组(按引用)
li_n = UpperBound(ltctn) // 条目数;空时为 0
DESTROY ltct
双轨实测:SetTraceFileName 与 BuildModel 均返回 Success!,EntryList 能正常回填数组。
条目数与内容的实测对照
样本 .trc 的内容UpperBound(EntryList)
2 个空块,无任何 TraceUser0
1 个块 + 2 条 TraceUser2
1 个块 + ActRoutine! + ActError! + 1 条 TraceUser + 1 条 TraceError4
条目数随内容变化,所以「条目数 = 块数」这个直觉是不成立的。
★一个编译期陷阱:TraceTreeNode 是基类
如果你想顺着条目往下取类名、对象号,会撞上这个:
ls_c = ltctn.ClassName // C0019: Incompatible property classname for type tracetreenode
ls_n = ltctn.name // C0019
ll_o = ltctn.ObjectID // C0019
ClassName / name / ObjectID / EnterTimerValue 这些属性不在基类 TraceTreeNode 上,而在子类 TraceTreeRoutine / TraceTreeObject / TraceTreeGarbageCollect 上(帮助里 Profiling 那个示例的写法是先 TraceTreeRoutine lx; lx = ltct_node 再取属性)。
未实测项:子类赋值与逐层遍历本轮没有做。所以本文只给「拿到顶层条目数」这一层,更深的遍历请以帮助里的 TraceTreeRoutine 示例为准,并在你的环境里自己验证一遍。
另注:TraceTree 之外还有同族的 Profiling 对象(面向性能分析),本文未实测。
十三、封装成一个能直接用的工具类
以下代码在 PB10 与 PB12.5 上双轨实机跑通(import 0 错 + 全量重建 0 错 + pbtest PASS + 静态检查 0 错 0 提醒)。11 个方法,薄封装,重点是第 4 节那两条防御。
$PBExportHeader$nvo_pbtrace.sru
forward
global type nvo_pbtrace from nonvisualobject
end type
end forward
global type nvo_pbtrace from nonvisualobject
end type
global nvo_pbtrace nvo_pbtrace
type prototypes
end prototypes
type variables
end variables
forward prototypes
function string of_lab (ErrorReturn ae_ret)
function ErrorReturn of_open (string as_file, TimerKind atk)
function ErrorReturn of_begin (string as_id)
function ErrorReturn of_user (long al_info, string as_msg)
function ErrorReturn of_error (long al_sev, string as_msg)
function ErrorReturn of_end ()
function ErrorReturn of_close ()
function boolean of_prepare (string as_file)
function boolean of_safe_open (string as_file, TimerKind atk)
function integer of_tree_entries (string as_file)
function string of_self_test (string as_dir)
end prototypes
function string of_lab (ErrorReturn ae_ret)
// 把 ErrorReturn 枚举翻译成可读名字
// 注意:long(枚举) 实测恒为 0、string(枚举) 恒为空串,枚举不可转换
string ls
if IsNull(ae_ret) then
return 'NULL'
end if
if ae_ret = Success! then
ls = 'Success!'
elseif ae_ret = FileNotOpenError! then
ls = 'FileNotOpenError!'
elseif ae_ret = FileOpenError! then
ls = 'FileOpenError!'
elseif ae_ret = FileAlreadyOpenError! then
ls = 'FileAlreadyOpenError!'
elseif ae_ret = FileCloseError! then
ls = 'FileCloseError!'
elseif ae_ret = TraceStartedError! then
ls = 'TraceStartedError!'
elseif ae_ret = TraceNotStartedError! then
ls = 'TraceNotStartedError!'
elseif ae_ret = EnterpriseOnlyFeature! then
ls = 'EnterpriseOnlyFeature!'
else
ls = 'UNKNOWN'
end if
return ls
end function
function ErrorReturn of_open (string as_file, TimerKind atk)
// timer 取值:TimerNone! / Clock! / Process! / Thread!
// 帮助文档里写的 TimeNone! 是错的,编译报 C0060
return TraceOpen(as_file, atk)
end function
function ErrorReturn of_begin (string as_id)
return TraceBegin(as_id)
end function
function ErrorReturn of_user (long al_info, string as_msg)
// info 是 long,会按 4 字节小端写进 .trc;message 里可以有中文
return TraceUser(al_info, as_msg)
end function
function ErrorReturn of_error (long al_sev, string as_msg)
// severity 是 long;只有先 TraceEnableActivity(ActError!) 才会落盘
return TraceError(al_sev, as_msg)
end function
function ErrorReturn of_end ()
return TraceEnd()
end function
function ErrorReturn of_close ()
// 数据是缓冲到 Close 才落盘的;失败态下调它会让进程崩(0xC0000005)
return TraceClose()
end function
function boolean of_prepare (string as_file)
// 开跟踪前的体检:不通过就绝对不要调 TraceOpen
// 因为一次失败的 TraceOpen 会把本会话的 Trace 状态污染成「错误粘连」
string ls_dir
if IsNull(as_file) then
return false
end if
if Len(Trim(as_file)) = 0 then
return false
end if
ls_dir = Left(as_file, LastPos(as_file, '\'))
if Len(ls_dir) > 0 then
if not DirectoryExists(ls_dir) then
return false
end if
end if
return true
end function
function boolean of_safe_open (string as_file, TimerKind atk)
// 安全的开关:先体检、再删旧文件,最后才 Open
// 删旧文件是必须的——同一文件二次 TraceOpen 是「截断重写」,会把上一份日志清空
if not of_prepare(as_file) then
return false
end if
if FileExists(as_file) then
FileDelete(as_file)
end if
return (TraceOpen(as_file, atk) = Success!)
end function
function integer of_tree_entries (string as_file)
// 回读 .trc:TraceTreeNode 是基类,ClassName / name / ObjectID 在子类上
// 直接对基类节点取这些属性编译不过(C0019),所以这里只取顶层条目数
TraceTree ltct
TraceTreeNode ltctn[]
integer li_n
if not FileExists(as_file) then
return -1
end if
ltct = create TraceTree
if not IsValid(ltct) then
return -1
end if
if ltct.SetTraceFileName(as_file) <> Success! then
destroy ltct
return -1
end if
if ltct.BuildModel() <> Success! then
destroy ltct
return -1
end if
ltct.EntryList(ltctn)
li_n = UpperBound(ltctn)
destroy ltct
return li_n
end function
function string of_self_test (string as_dir)
// 一条龙自检:开 → 启活动 → 打点 → 关 → 量体积 → 回读
string ls_file, ls_out
integer li_n
ls_file = as_dir + 'pb_trace_demo.trc'
if not of_safe_open(ls_file, TimerNone!) then
return 'SAFE_OPEN_FAIL'
end if
TraceEnableActivity(ActRoutine!)
TraceEnableActivity(ActError!)
of_begin('blk1')
of_user(101, 'USERMARK')
of_error(900, 'ERRMARK')
of_end()
of_close()
ls_out = 'size=' + string(FileLength(ls_file))
li_n = of_tree_entries(ls_file)
ls_out = ls_out + ' entries=' + string(li_n)
ls_out = ls_out + ' lab=' + of_lab(Success!)
ls_out = ls_out + ' lab2=' + of_lab(FileOpenError!)
return ls_out
end function
自检的实机读数(双轨逐字一致):
size=868 entries=4 lab=Success! lab2=FileOpenError!
接到窗口上(调用示例)
下面这段是窗口按钮的脚本示例:它调用的每一个方法体都已在双轨 pbtest 中实测,但按钮脚本本身没有做界面级运行。
// 按钮一:跑一遍自检,把读数弹出来
nvo_pbtrace lnv
string ls_r
lnv = CREATE nvo_pbtrace
ls_r = lnv.of_self_test('C:\trace\')
DESTROY lnv
MessageBox('Trace 自检', ls_r)
// 按钮二:手动开一段跟踪,打两个点
nvo_pbtrace lnv2
lnv2 = CREATE nvo_pbtrace
IF lnv2.of_safe_open('C:\trace\manual.trc', TimerNone!) THEN
lnv2.of_begin('manual')
lnv2.of_user(1, '界面按钮被点击')
lnv2.of_end()
lnv2.of_close()
MessageBox('完成', '体积=' + String(FileLength('C:\trace\manual.trc')) + '~r~n' + &
'条目=' + String(lnv2.of_tree_entries('C:\trace\manual.trc')))
ELSE
MessageBox('注意', '开跟踪被拦下:先检查目录与文件名')
END IF
DESTROY lnv2
十四、坑清单
按「会不会报错」分成三类,这个分法本身就是这张清单的重点。
A. 完全不报错,静默给你错误的路径
[*]二次 TraceOpen 同一文件 = 截断重写,上一份日志连同内容一起消失(第 6 节)。
[*]TraceUser 的 info 是数字不是字符串,喂了字符串会编译失败,喂了 NULL 则整条记录被丢弃且返回值是 NULL。
[*]ActError! 没开时 TraceError 不落盘,返回值也不是 Success!,只能自己靠 = Success! 判。
[*]嵌套 TraceBegin 被静默忽略,第二次的标识不会出现在文件里(与帮助不符,第 10.4 节)。
[*].trc 在 TraceClose 之前是 0 字节或残缺,中途去读会得到一个误导性的结论(第 5 节)。
B. 报运行期错误,但 ErrorReturn 帮不上你
[*]ErrorReturn 不能转数字也不能转字符串,Long() 恒 0、String() 恒空串(第 7 节)。
[*]失败后错误粘连:一次失败的 TraceOpen 会让后续调用全返回那一个错误,还会用 FileAlreadyOpenError! 掩盖真实原因(第 4.1 节)。
C. 直接崩进程,异常都兜不住
[*]失败态下 TraceClose = 0xC0000005,TRY-CATCH 完全无效(第 4.2 / 4.3 节)。
[*]崩溃没有任何 PB 层提示,.pbtest 只给你一句「exited before writing a result」;窗口程序则是「窗口突然消失」。
D. 编译期才会暴露的(照抄帮助就会中)
[*]TimeNone! 不存在,真名是 TimerNone!(C0060)。
[*]ActCreateObject! / ActDestroyObject! 不存在,真名是 ActObjectCreate! / ActObjectDestroy!(C0060)。
[*]TraceUser 单参写法编译不过,必须两参(C0052)。
[*]ActivityKind / ActivityType 不能作参数类型名(C0001),直接传 ActXxx! 常量。
[*]枚举类型名区分大小写:errreturn 报 C0001,ErrorReturn 正常。
十五、PB12.5 差异
行为层面:两版一致。本文所有运行级结论(8 个函数的返回值、失败残留与崩溃、截断行为、活动开关的体积差、TraceUser / TraceError 的记录格式、TraceTree 回读、TimerKind 编码)在 PB10 与 PB12.5 上逐条比对,没有发现任何行为差异。
可复现的两点环境差异:
项PB10PB12.5
.trc 头部 host 字段INTEL 01 238INTEL 01 333(68 个样本零例外)
.trc 体积同一段代码 868 字节868 字节(一致)
关于体积:早期一版探针在两轨上量到 868 / 866 字节的 2 字节差,追下去发现差异来自 .trc 尾部符号表里记录的宿主可执行文件绝对路径长度(测试宿主每次运行会生成带时间戳与 PID 的临时目录),不是 PB 版本造成的。换成同一份最终代码后,两轨都是 868。
十六、实机验证情况
测试环境
[*]PowerBuilder 10 与 PowerBuilder 12.5(均为 32 位),Windows 10
[*]运行方式:ORCA pbl import → build rebuild --type full → PBVM .pbtest 驱动
[*]探针规模:本轮共 6 个 .pbtest 批次、约 60 个独立探针对象;每个崩溃类结论都是独立进程跑出来的一手读数
双轨实机读数(关键项)
探针PB10PB12.5
TraceOpen 后 / TraceClose 后体积0 / 6020 / 602
空文件名 OpenFileOpenError!FileOpenError!
失败后紧接着的调用FileAlreadyOpenError!FileAlreadyOpenError!
失败态下 TraceClose崩溃 3221225477崩溃 3221225477
TRY-CATCH 序列打点停在 T2,未捕获停在 T2,未捕获
TraceError(ActError! 关 / 开)UNKNOWN (620B) / Success! (644B)同
活动开关 2×2610 / 648 / 790 / 670614 / 650 / 788 / 668
Enable → Disable902 → 652904 → 654
空/300 字符/顺序/嵌套 TraceBegin全 Success!,1244B全 Success!,1242B
TraceUser(5, NULL) / TraceBegin(NULL)NULL / Success!同
TraceTree 回读set=Success! build=Success! entries=2同
交付对象自检size=868 entries=4 lab=Success!同
中文消息落盘命中 @290命中 @290
记录标记09 01 / 07 03 / 08 03 / 09 02同
静态检查
[*]编码检查:bad = 0(交付对象 GBK + CRLF + 无 BOM,首行 $PBExportHeader$ 齐全)。
[*]帮助库核对:0 错误、0 提醒(prototypes 11 条 / 实现 11 条)。
[*]本文代码在 PB10 与 PB12.5 双轨 import 0 错、全量重建 0 错。
未实测项(明确划出边界)
[*]★第二个 TraceOpen 失败的错误码:状态一旦被污染,后续调用统一返回 FileAlreadyOpenError!,所以「第二次失败本身是什么错」无法单独取得。第 4 节按「污染后的表现」如实描述。
[*]★TraceError 在 ActError! 未启用时返回的具体枚举值:因为 Long() 恒 0,取不出数值,只能说「不等于 Success!」。
[*]★ActLine! / ActProfile! / ActTrace! 的连带关系(开 ActLine! 是否连带开 ActRoutine! 等)只做了转述,未单独实测。
[*]★Profiling 对象与 TraceTree 的子类遍历(TraceTreeRoutine / TraceTreeObject / TraceTreeGarbageCollect 赋值取属性)未实测。
[*]★多实例 / 多线程同时跟踪未测,第 4 节的结论只在「单会话单状态机」前提下成立。
[*]★磁盘写满时的行为(帮助称「不产生错误但停止记录」)未测。
[*]★TraceTree 条目数与 .trc 记录之间的确切映射未穷举,第 12 节只给三组对照读数。
[*]★0xC4 / 0xC8 两个头部字段的语义未确定,官方未公开,本文只给观察值。
[*]★窗口按钮脚本未做界面级运行,其调用的 of_ 方法体全部双轨实测。
十七、写在最后
PowerBuilder 的 Trace 函数族是我见过「入门门槛最低、事故密度最高」的一组 API。8 个函数,签名一眼能背;但它的错误处理哲学是「把状态管理的责任 100% 交给调用者」:
[*]它不告诉你文件是什么时候写的(缓冲到 Close);
[*]它不告诉你第二次 Open 会清空(截断重写);
[*]它不让你把错误码记下来(枚举不可转换);
[*]一旦 Open 失败,它甚至不给你退出的机会(Close 直接崩)。
所以我给三条建议:
第一,把「体检」写在 TraceOpen 之前,而不是把「补救」写在失败之后。目录先查、文件名先查、旧文件先删——这三件小事做完,第 4 节整节的坑就与你无关了。
第二,判定「记录有没有进去」只看两个数:文件体积和 TraceTree 条目数。别去解析 .trc 文本,那是 UTF-16LE 加 NUL 填充的容器,String(blob) 在前 7 个字符就断了。真要读内容,走 TraceTree。
第三,日常跟踪只开 ActUser!。保持默认、只在关键位置 TraceUser,日志既有信息量又体积可控;需要排查运行时活动时再临时开 ActRoutine! / ActError!,用完 TraceDisableActivity 关掉。
最后留一个我这次没能做完的方向:TraceTree 的子类遍历与 Profiling 对象。理论上一份 .trc 能被还原成完整的调用树与耗时排序,这比「看日志」有用得多;但它的属性分布在 TraceTreeRoutine / TraceTreeObject 这些子类上,TraceTreeNode 基类上取不到(C0019),我本轮只走到「拿到顶层条目数」这一步。
如果你在项目里已经把 TraceTree 的遍历跑通了,非常欢迎在回帖里贴出来,我把它补进第十二节。本文把这一族的边界尽量钉死了,剩下没定论的那几条都列在第十六节,等你来补。 配套文章:PowerBuilder 运行跟踪全解:Trace 函数族 8 个函数的实测行为、失败残留与 .trc 文件结构(PB10 基准 · PB12.5 实测)
本楼附上文章全部对象的 PB10 兼容版示例包,解压打开即可运行。
一、包内是什么
把正文里的工具类原样搬进一个干净库,另加一个能直接跑起来的窗口 w_pbtrace_demo(4 个按钮,一按钮对应文章一处结论),删掉了与文章无关的一切。
包内清单:
[*]trace66.pbl / trace66.pbt —— 可直接打开的库与工程
[*]src/a_pbtrace_demo.sra —— 应用对象,Open 事件只做 open(w_pbtrace_demo),不连库
[*]src/w_pbtrace_demo.srw —— 演示窗口,1 个输出框 + 4 个按钮
[*]src/nvo_pbtrace.sru —— 工具类,把文章结论封成 11 个 of_ 方法
11 个方法一览:
方法作用对应文章
of_labErrorReturn 枚举转可读名第七节
of_openTraceOpen 直通第三、九节
of_beginTraceBegin第十节
of_userTraceUser第十节
of_errorTraceError第十一节
of_endTraceEnd第十节
of_closeTraceClose(落盘在这一步)第五节
of_prepare开之前先查:目录存在 + 路径非空第四节
of_safe_open唯一可行防御:先删旧文件再 Open第四节
of_tree_entriesTraceTree 回读条目数第十二节
of_self_test一条龙自检第十三节
二、环境与运行前提
本包零外部依赖——这是它和前面几篇 PBIDEA 示例包最大的不同:Trace 函数族与 TraceTree 都是 PowerBuilder 运行库自带能力,不需要任何第三方组件或附加运行库,也不连库。
[*]版本:PB10 为基准(包内 PBL 就是 PB10 格式),PB12.5 同样可编译可运行——本包双轨实跑验证过
[*]位数:32 位(PB 10 / 12.5 都是 32 位程序)
[*]系统:Windows 7 及以上
[*]依赖库:无,liblist 里只需要本包的 trace66.pbl
[*]数据库:不需要,SQLCA 不用配
[*]落盘目录:演示程序把 .trc 写到当前工作目录(GetCurrentDirectory()),运行前请确认该目录存在且可写——正文第四节那个「崩溃坑」的触发条件之一就是目录不存在
三、四个按钮分别看什么
按钮对应文章预期读数
1. 一条龙自检第十三节size=868 entries=4 lab=Success! lab2=FileOpenError!
2. 默认活动(只有 ActUser!)第五节体积 602 ~ 624 字节;Open 后长度=0、Close 后才落盘
3. 再开 ActRoutine! + ActError!第八节体积 648 ~ 790 区间,与按钮 2 的差就是开关生效的证据
4. TraceTree 回读第十二节entries=4,并演示 TraceTreeNode 基类陷阱
按钮 1 的那串读数在固定输入下逐字节稳定,PB10 与 PB12.5 两轨输出完全一致,可以直接当基准值对照。按钮 2 / 3 的体积是范围不是定值——它随打点内容与 timer 选项变化,请「看相对、不看绝对」。
按钮 4 为什么要专门演示一个编译不过的写法:正文第十二节实测,ltctn.ClassName / .name / .ObjectID 写成这样直接报 C0019(Incompatible property),因为 TraceTreeNode 是基类,这三个属性属于子类 TraceTreeRoutine / TraceTreeObject。包里 of_tree_entries 因此只取 UpperBound(EntryList),够用且不踩坑。
四、本包最重要的那条规矩
别写「先 TraceOpen 试试,失败就算了」这种代码。正文第四节实测:一次失败的 TraceOpen 之后,错误会「粘连」到本会话所有后续调用——紧接着一次「本该成功」的打开返回的是 FileAlreadyOpenError!,把真因挡住;更要命的是在失败态下再调 TraceClose,进程直接 0xC0000005 消失,TRY-CATCH 兜不住。
所以打开前只有两条路:of_prepare(判目录存在 + 路径非空非 null)或 of_safe_open(在此基础上先 FileDelete 再 TraceOpen)。后者同时规避了第二个坑——同一个文件二次 TraceOpen 会截断上一份日志(实测 640 → 624 字节),这也是「中文丢了」的真实原因,不是编码问题。
五、本包的四项验证(都在本机实跑过)
[*]PB10 ORCA 全量编译:0 错误
[*]对象齐全:包内 3 个源对象全部真正进入 PBL(防静默丢失)
[*]双轨运行复验:PB10 与 PB12.5 各跑一次 .pbtest,均 PASS,读数逐字一致(size=868 entries=4 lab=Success! lab2=FileOpenError!)
[*]源码编码:全部 GBK + CRLF + 无 BOM,首行 $PBExportHeader$
六、附件
附件:trace66_pb10.zip(PB10 兼容版 PBL + 3 个对象源码 + 使用说明)。
附注:.trc 是 UTF-16LE 文本容器,用记事本打开会看到乱码,这不是坏文件。头部 magic 是 PBTRACE;host 字段 PB10 一律 INTEL 01 238、PB12.5 一律 INTEL 01 333(68 个样本零例外),可以用它判版本。另外 TraceTree 在本机 PB10 / PB12.5 上都可用,没有出现 EnterpriseOnlyFeature! 返回值。
页:
[1]