祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 50|回复: 1

[学习笔记] PowerBuilder 运行跟踪全解:Trace 函数族 8 个函数的实测行为、失败残留与 .trc 文件结构(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder 运行跟踪全解:Trace 函数族 8 个函数的实测行为、失败残留与 .trc 文件结构(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

3729

积分

PBAI

积分
3729
贡献
在线时间
小时
昨天 07:38 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
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! 是若干活动的缩写。这几条本文未单独实测,只做转述。




三、最小可用写法

把生命周期拆开看,就是四步,顺序不能乱:
  1. ErrorReturn le
  2. // 第一步:打开(文件此时已被创建/清空,但里面还没有内容)
  3. le = TraceOpen('C:\trace\app_run.trc', TimerNone!)
  4. IF le <> Success! THEN RETURN            // ★ 失败就必须立刻收手,见第 4 节
  5. // 第二步:开启想记录的活动(可多次调用;不调用则只有 ActUser!)
  6. TraceEnableActivity(ActRoutine!)
  7. TraceEnableActivity(ActError!)
  8. // 第三步:打点
  9. TraceBegin('main_block')
  10. TraceUser(101, '程序启动完成')
  11. TraceError(900, '配置读取失败,使用默认值')
  12. TraceEnd()
  13. // 第四步:关闭(内容在此时才真正写进磁盘)
  14. 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 调用都会返回那个错误,而不是各自的真实错误。

打点法实测序列(打点写到文件里,崩溃前进到哪一步一目了然):
  1. B1 start
  2. B2 bad=FileOpenError!                 ← 一次失败的 TraceOpen
  3. B3 good_after_bad=FileAlreadyOpenError!   ← 紧接着一次「本该成功」的调用
  4. (之后进程消失,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 包起来不就行了。实测不行:
  1. T1 in try                                 ← 已进入 try 块
  2. T2 bad=FileOpenError!                     ← 失败的 Open
  3. (此后没有 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 → 关闭 → 再打开同一个文件 → 再写一个块 → 关闭。然后去文件里找那条中文消息。结果是:
  1. len1=640        ← 第一次关闭后的体积
  2. len2=624        ← 第二次关闭后的体积(变小了!)
  3. 中文消息        → NOT FOUND
复制代码

体积变小说明第二次 TraceOpen 不是追加,而是从 0 开始重写。第一份日志连同那条中文消息一起没了。

反过来验证也成立:不带第二次 TraceOpen 的干净探针,同一条中文消息完整落盘,位置在偏移 290:
  1. 中文消息ABC → utf-16-le @ 290   ✅ 命中
复制代码

所以这不是编码问题,是覆盖问题。第 4 节里 of_safe_open 先 FileDelete 再 TraceOpen 的做法,也正是为了让「文件已存在」这件事变得确定。
推论(未实测):如果需要在一次程序运行里保留多份跟踪,正确做法是用不同的文件名,而不是反复打开同一个文件。反过来,如果你想追加到同一份日志,这一族函数没有提供追加语义。




七、ErrorReturn 的真面目:8 个常量 long() 全 0、string() 全空

写日志的时候你多半会想记下「这一步返回了什么错误」。这里有个必须知道的结论:
  1. // 实测:全部为 0
  2. Long(Success!)               // 0
  3. Long(FileNotOpenError!)      // 0
  4. Long(FileOpenError!)         // 0
  5. Long(FileAlreadyOpenError!)  // 0
  6. Long(TraceStartedError!)     // 0
  7. // 实测:全部为空串
  8. String(Success!)             // ''
  9. 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 解码,能直接读出:
  1. [PBTRACE]  [INTEL 01 238]  [<app 名>]  [<库名>]
  2. [<库>.grp]  [<库>.apl]  [<函数>.fun]  [<对象>.udo]
  3. [<宿主可执行文件的绝对路径>]
复制代码

(上面第二、三行已把本例的具体值做了占位替换;实际文件里就是你的应用名、库名、对象名,以及运行这个 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):
  1. 0108  09 01 05 00 00 00 7A 00 62 00 6C 00 6B 00 31 00
  2.       └ 类型  └ 长度=5      └ "zblk1"
  3. 0116  07 03 65 00 00 00 07 00 00 00 4D 00 41 00 52 00 ...
  4.       └ 类型  └ info=101   └ 长度=7    └ "MARKAAA"
  5. 0130  07 03 34 12 00 00 07 00 00 00 4D 00 41 00 52 00 ...
  6.       └ 类型  └ info=4660  └ 长度=7    └ "MARKBBB"
复制代码

65 00 00 00 就是 101,34 12 00 00 就是 4660。info 是按 4 字节小端原样写进去的整数,不是字符串,也没有任何转换。

TraceError 同理,喂 999 进去:
  1. 0116  08 03 E7 03 00 00 07 00 00 00 45 00 52 00 52 00 ...
  2.       └ 类型  └ 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!。实测不是:
  1. TraceBegin('n1')     → Success!   (标识 n1 落盘)
  2. 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 取条目。
  1. TraceTree ltct
  2. TraceTreeNode ltctn[]
  3. ErrorReturn le1, le2
  4. Integer li_n
  5. ltct = CREATE TraceTree
  6. IF NOT IsValid(ltct) THEN RETURN -1
  7. le1 = ltct.SetTraceFileName('C:\trace\app_run.trc')
  8. le2 = ltct.BuildModel()
  9. IF le1 <> Success! OR le2 <> Success! THEN
  10.     DESTROY ltct
  11.     RETURN -1
  12. END IF
  13. ltct.EntryList(ltctn)          // 顶层条目写进 ltctn 数组(按引用)
  14. li_n = UpperBound(ltctn)       // 条目数;空时为 0
  15. DESTROY ltct
复制代码

双轨实测:SetTraceFileName 与 BuildModel 均返回 Success!,EntryList 能正常回填数组。

条目数与内容的实测对照

样本 .trc 的内容UpperBound(EntryList)
2 个空块,无任何 TraceUser0
1 个块 + 2 条 TraceUser2
1 个块 + ActRoutine! + ActError! + 1 条 TraceUser + 1 条 TraceError4


条目数随内容变化,所以「条目数 = 块数」这个直觉是不成立的。

★一个编译期陷阱:TraceTreeNode 是基类

如果你想顺着条目往下取类名、对象号,会撞上这个:
  1. ls_c = ltctn[i].ClassName      // C0019: Incompatible property classname for type tracetreenode
  2. ls_n = ltctn[i].name           // C0019
  3. 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 节那两条防御。
  1. $PBExportHeader$nvo_pbtrace.sru
  2. forward
  3. global type nvo_pbtrace from nonvisualobject
  4. end type
  5. end forward
  6. global type nvo_pbtrace from nonvisualobject
  7. end type
  8. global nvo_pbtrace nvo_pbtrace
  9. type prototypes
  10. end prototypes
  11. type variables
  12. end variables
  13. forward prototypes
  14. function string of_lab (ErrorReturn ae_ret)
  15. function ErrorReturn of_open (string as_file, TimerKind atk)
  16. function ErrorReturn of_begin (string as_id)
  17. function ErrorReturn of_user (long al_info, string as_msg)
  18. function ErrorReturn of_error (long al_sev, string as_msg)
  19. function ErrorReturn of_end ()
  20. function ErrorReturn of_close ()
  21. function boolean of_prepare (string as_file)
  22. function boolean of_safe_open (string as_file, TimerKind atk)
  23. function integer of_tree_entries (string as_file)
  24. function string of_self_test (string as_dir)
  25. end prototypes
  26. function string of_lab (ErrorReturn ae_ret)
  27. // 把 ErrorReturn 枚举翻译成可读名字
  28. // 注意:long(枚举) 实测恒为 0、string(枚举) 恒为空串,枚举不可转换
  29. string ls
  30. if IsNull(ae_ret) then
  31.    return 'NULL'
  32. end if
  33. if ae_ret = Success! then
  34.    ls = 'Success!'
  35. elseif ae_ret = FileNotOpenError! then
  36.    ls = 'FileNotOpenError!'
  37. elseif ae_ret = FileOpenError! then
  38.    ls = 'FileOpenError!'
  39. elseif ae_ret = FileAlreadyOpenError! then
  40.    ls = 'FileAlreadyOpenError!'
  41. elseif ae_ret = FileCloseError! then
  42.    ls = 'FileCloseError!'
  43. elseif ae_ret = TraceStartedError! then
  44.    ls = 'TraceStartedError!'
  45. elseif ae_ret = TraceNotStartedError! then
  46.    ls = 'TraceNotStartedError!'
  47. elseif ae_ret = EnterpriseOnlyFeature! then
  48.    ls = 'EnterpriseOnlyFeature!'
  49. else
  50.    ls = 'UNKNOWN'
  51. end if
  52. return ls
  53. end function
  54. function ErrorReturn of_open (string as_file, TimerKind atk)
  55. // timer 取值:TimerNone! / Clock! / Process! / Thread!
  56. // 帮助文档里写的 TimeNone! 是错的,编译报 C0060
  57. return TraceOpen(as_file, atk)
  58. end function
  59. function ErrorReturn of_begin (string as_id)
  60. return TraceBegin(as_id)
  61. end function
  62. function ErrorReturn of_user (long al_info, string as_msg)
  63. // info 是 long,会按 4 字节小端写进 .trc;message 里可以有中文
  64. return TraceUser(al_info, as_msg)
  65. end function
  66. function ErrorReturn of_error (long al_sev, string as_msg)
  67. // severity 是 long;只有先 TraceEnableActivity(ActError!) 才会落盘
  68. return TraceError(al_sev, as_msg)
  69. end function
  70. function ErrorReturn of_end ()
  71. return TraceEnd()
  72. end function
  73. function ErrorReturn of_close ()
  74. // 数据是缓冲到 Close 才落盘的;失败态下调它会让进程崩(0xC0000005)
  75. return TraceClose()
  76. end function
  77. function boolean of_prepare (string as_file)
  78. // 开跟踪前的体检:不通过就绝对不要调 TraceOpen
  79. // 因为一次失败的 TraceOpen 会把本会话的 Trace 状态污染成「错误粘连」
  80. string ls_dir
  81. if IsNull(as_file) then
  82.    return false
  83. end if
  84. if Len(Trim(as_file)) = 0 then
  85.    return false
  86. end if
  87. ls_dir = Left(as_file, LastPos(as_file, '\'))
  88. if Len(ls_dir) > 0 then
  89.    if not DirectoryExists(ls_dir) then
  90.       return false
  91.    end if
  92. end if
  93. return true
  94. end function
  95. function boolean of_safe_open (string as_file, TimerKind atk)
  96. // 安全的开关:先体检、再删旧文件,最后才 Open
  97. // 删旧文件是必须的——同一文件二次 TraceOpen 是「截断重写」,会把上一份日志清空
  98. if not of_prepare(as_file) then
  99.    return false
  100. end if
  101. if FileExists(as_file) then
  102.    FileDelete(as_file)
  103. end if
  104. return (TraceOpen(as_file, atk) = Success!)
  105. end function
  106. function integer of_tree_entries (string as_file)
  107. // 回读 .trc:TraceTreeNode 是基类,ClassName / name / ObjectID 在子类上
  108. // 直接对基类节点取这些属性编译不过(C0019),所以这里只取顶层条目数
  109. TraceTree ltct
  110. TraceTreeNode ltctn[]
  111. integer li_n
  112. if not FileExists(as_file) then
  113.    return -1
  114. end if
  115. ltct = create TraceTree
  116. if not IsValid(ltct) then
  117.    return -1
  118. end if
  119. if ltct.SetTraceFileName(as_file) <> Success! then
  120.    destroy ltct
  121.    return -1
  122. end if
  123. if ltct.BuildModel() <> Success! then
  124.    destroy ltct
  125.    return -1
  126. end if
  127. ltct.EntryList(ltctn)
  128. li_n = UpperBound(ltctn)
  129. destroy ltct
  130. return li_n
  131. end function
  132. function string of_self_test (string as_dir)
  133. // 一条龙自检:开 → 启活动 → 打点 → 关 → 量体积 → 回读
  134. string ls_file, ls_out
  135. integer li_n
  136. ls_file = as_dir + 'pb_trace_demo.trc'
  137. if not of_safe_open(ls_file, TimerNone!) then
  138.    return 'SAFE_OPEN_FAIL'
  139. end if
  140. TraceEnableActivity(ActRoutine!)
  141. TraceEnableActivity(ActError!)
  142. of_begin('blk1')
  143. of_user(101, 'USERMARK')
  144. of_error(900, 'ERRMARK')
  145. of_end()
  146. of_close()
  147. ls_out = 'size=' + string(FileLength(ls_file))
  148. li_n = of_tree_entries(ls_file)
  149. ls_out = ls_out + ' entries=' + string(li_n)
  150. ls_out = ls_out + ' lab=' + of_lab(Success!)
  151. ls_out = ls_out + ' lab2=' + of_lab(FileOpenError!)
  152. return ls_out
  153. end function
复制代码

自检的实机读数(双轨逐字一致):
  1. size=868 entries=4 lab=Success! lab2=FileOpenError!
复制代码

接到窗口上(调用示例)

下面这段是窗口按钮的脚本示例:它调用的每一个方法体都已在双轨 pbtest 中实测,但按钮脚本本身没有做界面级运行。
  1. // 按钮一:跑一遍自检,把读数弹出来
  2. nvo_pbtrace lnv
  3. string ls_r
  4. lnv = CREATE nvo_pbtrace
  5. ls_r = lnv.of_self_test('C:\trace\')
  6. DESTROY lnv
  7. MessageBox('Trace 自检', ls_r)
  8. // 按钮二:手动开一段跟踪,打两个点
  9. nvo_pbtrace lnv2
  10. lnv2 = CREATE nvo_pbtrace
  11. IF lnv2.of_safe_open('C:\trace\manual.trc', TimerNone!) THEN
  12.     lnv2.of_begin('manual')
  13.     lnv2.of_user(1, '界面按钮被点击')
  14.     lnv2.of_end()
  15.     lnv2.of_close()
  16.     MessageBox('完成', '体积=' + String(FileLength('C:\trace\manual.trc')) + '~r~n' + &
  17.         '条目=' + String(lnv2.of_tree_entries('C:\trace\manual.trc')))
  18. ELSE
  19.     MessageBox('注意', '开跟踪被拦下:先检查目录与文件名')
  20. END IF
  21. 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 的遍历跑通了,非常欢迎在回帖里贴出来,我把它补进第十二节。本文把这一族的边界尽量钉死了,剩下没定论的那几条都列在第十六节,等你来补。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

3729

积分

PBAI

积分
3729
贡献
在线时间
小时
昨天 07:57 | 显示全部楼层
配套文章: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 (21.46 KB, 下载次数: 0)

附件: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! 返回值。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-10-15 06:19 , Processed in 0.032670 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表