马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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 帮助一致):
| 函数 | 签名 | 参数类型 | | TraceOpen | TraceOpen ( filename, timer ) | string + TimerKind | | TraceClose | TraceClose ( ) | 无 | | TraceBegin | TraceBegin ( identifier ) | string | | TraceEnd | TraceEnd ( ) | 无 | | TraceEnableActivity | TraceEnableActivity ( activity ) | 活动常量 | | TraceDisableActivity | TraceDisableActivity ( activity ) | 活动常量 | | TraceUser | TraceUser ( info, message ) | long + string | | TraceError | TraceError ( severity, message ) | long + string |
八个函数全部返回 ErrorReturn。
顺带排掉一个坑:帮助库里还有一个 TraceFile = 'tracefile_name',那是 JDBC 驱动管理器跟踪的数据库参数(写进 SQLCA.DBParm 的),和本文这 8 个函数没有任何关系。搜「Trace」搜到它别顺手抄进代码。
帮助文档实测写错/写漏的四处
这四处不是「翻译问题」,是照抄就编译不过:
| # | 帮助里的写法 | 实测结果 | 正确写法 | | 1 | TraceOpen 的 timer 取值写 TimeNone! | import 报 C0060 Illegal enumerated constant(双轨一致) | TimerNone! | | 2 | TraceEnableActivity 示例里的 ActCreateObject! / ActDestroyObject! | import 报 C0060(双轨一致) | ActObjectCreate! / ActObjectDestroy! | | 3 | TraceUser 的示例写成单参 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 失败 → Close | A2 open_empty=FileOpenError! | 3221225477 | | B | 目录不存在 Open 失败 → 先做一次正常调用 → Close | B3 good_after_bad=FileAlreadyOpenError! | 3221225477 | | C | 目录不存在 Open 失败 → Close | C2 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 之后立刻 FileExists | true(文件已被创建) | | TraceOpen 之后立刻 FileLength | 0 | | TraceClose 之后 FileExists | true | | TraceClose 之后 FileLength | 602 |
所以顺序是:Open 建空文件 → 全程写内存缓冲 → Close 一次性落盘。
还有一条容易误判的行为:
| 场景 | TraceBegin 返回值 | | TraceClose 之后继续 TraceBegin | FileNotOpenError! | | TraceClose 之后继续 TraceClose | FileNotOpenError! |
注意这里没有崩。也就是说:成功的 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! | 0 | 1 | 无时间字段 | | Clock! | 23 | 2 | 墙钟(相对机器启动),微秒 | | Process! | 0 | 3 | 本进程经过时间 | | Thread! | 0 | 4 | 本线程经过时间 |
同一个「打 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 解码,能直接读出:
- [PBTRACE] [INTEL 01 238] [<app 名>] [<库名>]
- [<库>.grp] [<库>.apl] [<函数>.fun] [<对象>.udo]
- [<宿主可执行文件的绝对路径>]
复制代码
(上面第二、三行已把本例的具体值做了占位替换;实际文件里就是你的应用名、库名、对象名,以及运行这个 PB 程序的 EXE 完整路径。)
结尾那一大段是符号表:库名、对象名、函数名,以及宿主可执行文件的绝对路径。开头几个字段:
| 偏移 | 内容 | 实测值 | | 0x000 | magic | PBTRACE(UTF-16LE,7 字符) | | ~0x032 | host 标识 | PB10 = INTEL 01 238;PB12.5 = INTEL 01 333 | | 0x0C4 | 4 字节小端整数 | 随内容增长(668 B 文件 = 330;1244 B 文件 = 904) | | 0x0C8 | 4 字节小端整数 | 与文件尾相差 182 ~ 184 字节 | | 0x0D8 | 4 字节小端整数 | TimerNone! 下为 0 | | 0x0E0 | 4 字节小端整数 | timer 编码,见第 9 节 | | 0x0E8 | 4 字节小端整数 | 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 字节标记 | 结构 | | TraceBegin | 09 01 | 09 01 + 长度(4B) + 块标识(UTF-16LE) | | TraceUser | 07 03 | 07 03 + info(4B 小端) + 长度(4B) + 消息(UTF-16LE) | | TraceError | 08 03 | 08 03 + severity(4B 小端) + 长度(4B) + 消息(UTF-16LE) | | 符号表项 | 09 02 | 09 02 + 4B + 长度(4B) + 名字(UTF-16LE) |
拿一个真实片段对照(TraceBegin('zblk1') + 两次 TraceUser):
- 0108 09 01 05 00 00 00 7A 00 62 00 6C 00 6B 00 31 00
- └ 类型 └ 长度=5 └ "zblk1"
- 0116 07 03 65 00 00 00 07 00 00 00 4D 00 41 00 52 00 ...
- └ 类型 └ info=101 └ 长度=7 └ "MARKAAA"
- 0130 07 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 进去:
- 0116 08 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 个空块,无任何 TraceUser | 0 | | 1 个块 + 2 条 TraceUser | 2 | | 1 个块 + ActRoutine! + ActError! + 1 条 TraceUser + 1 条 TraceError | 4 |
条目数随内容变化,所以「条目数 = 块数」这个直觉是不成立的。
★一个编译期陷阱:TraceTreeNode 是基类
如果你想顺着条目往下取类名、对象号,会撞上这个:
- ls_c = ltctn[i].ClassName // C0019: Incompatible property classname for type tracetreenode
- ls_n = ltctn[i].name // C0019
- ll_o = ltctn[i].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 上逐条比对,没有发现任何行为差异。
可复现的两点环境差异:
| 项 | PB10 | PB12.5 | | .trc 头部 host 字段 | INTEL 01 238 | INTEL 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 个独立探针对象;每个崩溃类结论都是独立进程跑出来的一手读数
双轨实机读数(关键项)
| 探针 | PB10 | PB12.5 | | TraceOpen 后 / TraceClose 后体积 | 0 / 602 | 0 / 602 | | 空文件名 Open | FileOpenError! | FileOpenError! | | 失败后紧接着的调用 | FileAlreadyOpenError! | FileAlreadyOpenError! | | 失败态下 TraceClose | 崩溃 3221225477 | 崩溃 3221225477 | | TRY-CATCH 序列打点 | 停在 T2,未捕获 | 停在 T2,未捕获 | | TraceError(ActError! 关 / 开) | UNKNOWN (620B) / Success! (644B) | 同 | | 活动开关 2×2 | 610 / 648 / 790 / 670 | 614 / 650 / 788 / 668 | | Enable → Disable | 902 → 652 | 904 → 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 的遍历跑通了,非常欢迎在回帖里贴出来,我把它补进第十二节。本文把这一族的边界尽量钉死了,剩下没定论的那几条都列在第十六节,等你来补。 |