feat(python): 在 app 进程里起 40250 脚本层(2P step 4,macOS)

移植 UserInterface/RunMainScript 到 platform/ScriptLib/PythonBoot:40250 把
CPythonLauncher 放在 Main() 的栈上,这里进程归 Godot,launcher 必须活过创建它的
那次调用,所以拆成 Start / RunMainScript / RunLine / Stop。37 个模块初始化函数里
只调已移植的 initpack(),system.py 因此仍停在第一个 import。

脚本报错能传回 GDScript,靠的是 platform/EterBase/Debug.cpp 实现 LogBox/LogBoxf:
ScriptLib 的 Traceback() 在 PyErr_Fetch 之后只剩这一条路,LastLogBoxMessage()
(platform/EterBase/LogBox.h)是 traceback 唯一幸存的地方。

GDScript 侧新增 Metin2PythonHost(python_host_node.{h,cpp})——唯一同时认识
godot-cpp 和 port 树的单元,PythonBoot 保持 godot-free。检查脚本
python_host_check.gd 分两段:解释器 + 标准库那段不需要 40250 的 pack,哪个平台都
能跑;system.py 那段需要 pack,没有就跳过。桌面入口 python_host_test.gd,真机入口
MT_TEST_MODE=python(导出模板不接受 --script)。

验证(macOS):标准库可 import、codec 注册表可用、sys.path 非空,system.py 停在和
ctest port.python_launcher 完全相同的 ImportError: No module named app;
ctest 27/27。Android/iOS 进程内未做,正式启动路径仍不起解释器(2V0)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 10:43:10 +09:00
co-authored by Claude Opus 5
parent e926759ff5
commit 0a1cdf3d3e
16 changed files with 638 additions and 76 deletions
+1
View File
@@ -499,3 +499,4 @@
{"time": "2026-09-23T22:20:00Z", "event": "scope_decision", "unit": "目标平台收窄为 macOS arm64 + Android arm64(Linux / Windows 不做)", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "decision": {"by": "user", "statement": "Linux 和 Windows 不考虑,目前主要支持 macOS arm64 和 Android arm64", "ios": "iOS arm64 保留为交叉编译干净的次要目标,不作为验收目标(我的判断,待用户确认)", "mingw": "仓库里的 mingw-w64 交叉编译只留作可移植性门禁——它这次抓到了 UserInterface/StdAfx.h 的真问题——不代表支持 Windows,不为它投入额外工作,也不阻塞任何步骤"}, "docs": ["docs/PORT-PLAN.md: 第 1 节新增范围段;批次 2A step 3 平台列表;批次 2P step 1/2 措辞;step 4 由「五个平台」改为「两个目标平台」,Linux/Windows 两行划掉并保留原因备查;总表 2P 行", "docs/PLATFORMS.md: 新增 Scope 段", "docs/THIRD-PARTY.md: CPython 的 Windows 段由「not built yet」改为 out of scope + 原因备查", "docs/PYTHON-EMBED-EVAL.md: 下一步 1/3 两条"], "tests": "仅文档改动,无代码改动;port_map.py check 0 error / 1 tag;参考 EterPack.cpp 的 8 个 pack key 值逐个 git grep --cached -w,均未入库", "not_done": "2P step 4 剩余项仍在:Android 真机 APK 进程内的 3c 沙盒落地,以及 app 进程里跑 system.py → prototype.RunApp()"}
{"time": "2026-09-23T22:35:00Z", "event": "known_issue", "unit": "Android GBK 编解码挂起;优先保证 macOS 可用", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "decision": {"by": "user", "statement": "先不管这个问题了,先保证 macOS 能正常使用,Android 后期再想办法"}, "finding": {"symptom": "build-android.sh 默认 API 24 下 extension/src/net/text_codec.cpp 编译失败:use of undeclared identifier 'iconv_open' / 'iconv' / 'iconv_close'", "root_cause": "text_codec.cpp:10 用 __has_include(<iconv.h>) 探测。NDK 里该头在任何 API 等级都存在,但声明被 __BIONIC_AVAILABILITY_GUARD(28) 挡住,探测方式与 bionic 的可用性模型不匹配", "measured": "NDK 28.2.13676358 clang++ -fsyntax-only:--target=aarch64-linux-android24 报 3 处 undeclared;android28 通过", "why_api_bump_is_wrong": "NDK iconv.h 注释写明 bionic 只支持 utf8/ascii/usascii/utf16be/utf16le/utf32be/utf32le/wchart,没有 GB2312/GBK。升到 28 后 iconv_open(\"GB2312\",\"UTF-8\") 运行期返回 -1,encode_one()/decode_wire() 对所有输入都返回 false,连纯 ASCII 也被拒,比现有 #else 的 ASCII 回退分支(text_codec.cpp:104-127)更糟。附带成本:APK min_sdk 需 >=28;config/android/pyconfig.h 是 API 24 下 configure 的(HAVE_LANGINFO_H 因 nl_langinfo 需 API 26 被关掉),要重跑 gen_pyconfig.sh android 并重验内建模块表", "future_fix": "1) 探测改为 #if __has_include(<iconv.h>) && !defined(__ANDROID__),不要写 __ANDROID_API__ >= 28;2) Android 自带 GBK<->Unicode 表(约 22k 码位)。40250 原客户端走 Windows MultiByteToWideChar(CP949/CP936),跨平台无对应物;macOS 能用只是系统 libiconv 恰好带 GBK"}, "docs": ["docs/PORT-PLAN.md 新增「已知问题:Android 的 GBK 编解码(挂起,2026-09-23)」小节"], "tests": "仅文档改动;诊断由 NDK clang++ -fsyntax-only 双 API 等级实测得出", "not_done": "text_codec.cpp 未改动;Android 扩展构建目前仍是红的"}
{"time": "2026-09-23T23:30:00Z", "event": "test_fix", "unit": "net 测试去 flaky:net_classic_session_test + net_loopback_test", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "trigger": "macOS 全量 ctest 连续两次各挂 1 个 net 测试,且每次挂的不是同一个(第一次 net.loopback_flow,第二次 net.classic_session)", "root_causes": ["accept_loopback(): poll() 的 `<= 0` 把 EINTR 当成对端从未连接;且单片 500ms 超时在有负载时不够。50 轮压测里 4 次失败全部源于此", "net_classic_session_test.cpp:570 的 pump 循环条件抄自上面的死端口用例,但此处 connect 会成功,stage 先经过 LoggingIn,循环提前一步退出", "同文件 5 处手写 pump 循环只给 10-20ms 等真实 loopback socket(非阻塞 connect 完成 / connect 被拒 / 对端 FIN)", "net_loopback_test.cpp 10 处 run(..., N, []{return false;}) 是固定空转而非等待条件,等不够就断言失败"], "fixes": ["accept_loopback() 改为 EINTR 重试 + 5s 截止时间(listener 本就是非阻塞,超时后 accept 不会挂住)", "新增 pump_until(session, want, max_ms=2000),替换全部 5 处手写循环;等的是断言真正关心的那个状态", "替换 replacement PHASE_LOGIN 处的单次 pump(前一条 CHECK 已经断言过「不在同一帧」,这里只需要后续某次 pump 收到)", "net_loopback_test 的 10 处固定空转改为条件等待;服务端批量收包用该批最后一个包的标志作条件(TCP 有序,最后一个到了说明前面都到了),客户端事件用 decltype 声明的本地累加器在谓词里 drain"], "speedup": {"net_classic_session_test": "61.2s -> 3.5s:两处 this-*-host-must-not-resolve.invalid 换成带空标签的名字。本机解析器不按 RFC 6761 本地拒绝 .invalid,每次发往上游等满 30s 超时;空标签让 getaddrinfo 在语法层返回同一个 EAI_NONAME,实测 <10ms", "net_loopback_test": "固定空转改条件退出后单跑 0.94s", "ctest_total": "78.7s -> 8.7s"}, "tests": "net_classic_session_test 100/100(修复前 50 轮挂 4 轮);net_loopback_test 100/100(修复前单独跑第 7 轮即挂);全量 ctest 27/27;port_map.py check 0 error;参考 EterPack.cpp 的 8 个 pack key 值逐个 git grep --cached -w,均未入库", "not_done": "只改测试,未改产品代码。这些都是测试自身的计时/错误处理缺陷,不是 mtnet 的行为问题"}
{"time": "2026-09-23T11:00:00Z", "event": "port_round", "unit": "PORT-PLAN 批次 2P step 4 (macOS): the 40250 script layer starts inside the Godot process", "ported": [], "adapted": ["UserInterface/UserInterface.cpp:RunMainScript -> extension/src/platform/ScriptLib/PythonBoot.cpp (Start/RunMainScript/RunLine/Stop; 40250 keeps CPythonLauncher on Main()'s stack, Godot owns this process so the launcher has to outlive the call that starts it)", "EterBase/Debug.cpp:LogBox, EterBase/Debug.cpp:LogBoxf -> extension/src/platform/EterBase/Debug.cpp (stderr plus LastLogBoxMessage(); ScriptLib Traceback() reports through LogBoxf after PyErr_Fetch has cleared the exception, so this is the only place a script traceback survives)"], "deleted": [], "divergent": ["RunMainScript RegisterCommandLine passes the command line through unchanged: the -cs/-ncs auto-login decoding needs EterBase/Utils.cpp SplitLine and EterLib/Util.cpp base64_decode, both declared but not ported (批次 2V1). Nothing in this host passes -cs today.", "RunMainScript drops '--pause-before-create-window' (system(\"pause\")) and the XTrap init check: no console and no XTrap on macOS/Android.", "Of the 37 module initializers RunMainScript calls, only initpack() is called — the other 36 are 2V slices."], "needs_live": [], "staging": {"native": "extension/src/platform/ScriptLib/PythonBoot.{h,cpp} (godot-free, placed under ScriptLib because that is the directory port_logic/port_platform drop when `mtpython` is absent; compiled with the UserInterface library's definitions so its `#ifdef _DISTRIBUTE` gives __DEBUG__ = 0). extension/src/platform/EterBase/LogBox.h. extension/src/python_host_node.{h,cpp} -> GDScript class Metin2PythonHost (start/is_running/run_main_script/run_line/stop; each returns \"\" or the reason, and a build without the interpreter answers with a distinguishable message instead of failing to register).", "gdscript": "project/python_host_check.gd in two halves — the interpreter + stdlib half needs no 40250 pack and runs on any platform, the system.py half needs the pack and is skipped without it. project/python_host_test.gd (--script) is the desktop entry; MT_TEST_MODE=python in project/client_main.gd is the on-device entry, since export templates do not accept --script.", "boot": "Metin2PythonHost.start(\"\") stages the stdlib through Metin2Python.stdlib_path(), which until now had no caller outside python_stdlib_test.gd."}, "status_note": "macOS only. Android/iOS in-process is untouched: the APK cross-build is blocked on the GBK iconv issue (see the PORT-PLAN known-issue section, deferred by the user 2026-09-23), and a device has no 40250 Client/pack, so on a phone only the interpreter + stdlib half of the check can run. prototype.RunApp() is 2V0: system.py still stops at its first import. The real boot path (AppFlow) does not start the interpreter.", "tests": ["godot --headless --path project --script python_host_test.gd -> PASS: interpreter starts in-process, is_running(), a second start() is a no-op, `import os, string, types, codecs`, u'\\xe4'.encode('cp1252'), sys.path non-empty, system.py stops at ImportError: No module named app (identical to ctest port.python_launcher), stop() tears it down", "MT_TEST_MODE=python godot --headless --path project -> same result through client_main.gd", "ctest --test-dir build -> 27/27 passed (14.51s)"], "not_done": "Android/iOS 进程内验证;正式启动路径起解释器(2V0);RunMainScript 的 -cs 命令行解码(2V1)", "commit": "2P-step4-macos"}
+69
View File
@@ -0,0 +1,69 @@
{
"reference": "EterBase/Debug.cpp",
"reference_sha256": "af7a6cba9e6d83511a40f596629f85f1b770b510fc0db1d9cbefcec88ecb15ec",
"priority": "P4",
"contracts": [],
"functions": {
"SetLogLevel": {
"status": "TODO"
},
"Log": {
"status": "TODO"
},
"Logn": {
"status": "TODO"
},
"Logf": {
"status": "TODO"
},
"Lognf": {
"status": "TODO"
},
"Trace": {
"status": "TODO"
},
"Tracen": {
"status": "TODO"
},
"Tracenf": {
"status": "TODO"
},
"Tracef": {
"status": "TODO"
},
"TraceError": {
"status": "TODO"
},
"TraceErrorWithoutEnter": {
"status": "TODO"
},
"LogBoxf": {
"impl": [
"extension/src/platform/EterBase/Debug.cpp:LogBoxf"
],
"status": "ADAPTED",
"note": "verbatim apart from two PORT fixes the 40250 build did not need: va_end (40250 leaks the va_list) and terminating szBuf, which _vsnprintf does not do on truncation.",
"test": "project/python_host_check.gd — system.py's ImportError reaches GDScript through it"
},
"LogBox": {
"impl": [
"extension/src/platform/EterBase/Debug.cpp:LogBox"
],
"status": "ADAPTED",
"note": "40250 pops a MessageBox on g_PopupHwnd and Tracen()s the text. No window here: the text goes to stderr and is kept in LastLogBoxMessage() (extension/src/platform/EterBase/LogBox.h), because ScriptLib/PythonLauncher.cpp Traceback() reports every failed script line through LogBoxf after PyErr_Fetch has already cleared the exception — this is the only place a traceback survives.",
"test": "project/python_host_check.gd — system.py's ImportError reaches GDScript through it"
},
"LogFile": {
"status": "TODO"
},
"LogFilef": {
"status": "TODO"
},
"OpenLogFile": {
"status": "TODO"
},
"OpenConsoleWindow": {
"status": "TODO"
}
}
}
@@ -1,73 +1,78 @@
{
"reference": "UserInterface/UserInterface.cpp",
"reference_sha256": "beed987e4b1de627acafe7ea6f5fde989b8f4b33c8643fe2abb0f7152ea4a9eb",
"priority": "P4",
"contracts": [],
"functions": {
"ApplicationSetErrorString": {
"status": "TODO"
},
"CheckPythonLibraryFilenames": {
"status": "TODO"
},
"ApplicationStringTable_Initialize": {
"status": "TODO"
},
"ApplicationStringTable_GetString": {
"status": "TODO"
},
"ApplicationStringTable_GetStringz": {
"status": "TODO"
},
"PackInitialize": {
"impl": [
"extension/src/platform/UserInterface/UserInterface.cpp:PackInitialize"
],
"status": "ADAPTED",
"note": "platform PackInitialize: Index pack names resolved to the on-disk .eix spelling case-insensitively; no LogBoxf; CTextFileLoader::SetCacheMode / CSoundData::SetPackMode not called (units not ported, neither affects pack registration)",
"test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects); extension/tests/proto_test.cpp (40250 locale/en item_proto/mob_proto through pack://)"
},
"RunMainScript": {
"status": "TODO"
},
"Main": {
"status": "TODO"
},
"CreateMetin2GameMutex": {
"status": "TODO"
},
"DestroyMetin2GameMutex": {
"status": "TODO"
},
"__ErrorPythonLibraryIsNotExist": {
"status": "TODO"
},
"__IsTimeStampOption": {
"status": "TODO"
},
"__PrintTimeStamp": {
"status": "TODO"
},
"__IsLocaleOption": {
"status": "TODO"
},
"__IsLocaleVersion": {
"status": "TODO"
},
"__IsOpenIDAuthKeyOption": {
"status": "TODO"
},
"__IsOpenIDTestOption": {
"status": "TODO"
},
"WinMain": {
"status": "TODO"
},
"GrannyError": {
"status": "TODO"
},
"Setup": {
"status": "TODO"
}
"reference": "UserInterface/UserInterface.cpp",
"reference_sha256": "beed987e4b1de627acafe7ea6f5fde989b8f4b33c8643fe2abb0f7152ea4a9eb",
"priority": "P4",
"contracts": [],
"functions": {
"ApplicationSetErrorString": {
"status": "TODO"
},
"CheckPythonLibraryFilenames": {
"status": "TODO"
},
"ApplicationStringTable_Initialize": {
"status": "TODO"
},
"ApplicationStringTable_GetString": {
"status": "TODO"
},
"ApplicationStringTable_GetStringz": {
"status": "TODO"
},
"PackInitialize": {
"impl": [
"extension/src/platform/UserInterface/UserInterface.cpp:PackInitialize"
],
"status": "ADAPTED",
"note": "platform PackInitialize: Index pack names resolved to the on-disk .eix spelling case-insensitively; no LogBoxf; CTextFileLoader::SetCacheMode / CSoundData::SetPackMode not called (units not ported, neither affects pack registration)",
"test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects); extension/tests/proto_test.cpp (40250 locale/en item_proto/mob_proto through pack://)"
},
"RunMainScript": {
"impl": [
"extension/src/platform/ScriptLib/PythonBoot.cpp:PythonBoot::RunMainScript"
],
"status": "ADAPTED",
"note": "40250 runs it on Main()'s stack and takes the CPythonLauncher by reference; Godot owns this process, so PythonBoot::Start holds the launcher and RunMainScript takes only the command line plus a std::string* for the text 40250 shows in LogBox's message box. Of the 37 module initializers only initpack() is called — the rest are 2V slices, which is why system.py still stops at its first import. RegisterCommandLine passes the line through unchanged: the -cs/-ncs auto-login decoding needs EterBase/Utils.cpp SplitLine and EterLib/Util.cpp base64_decode, both still TODO (批次 2V1). '--pause-before-create-window' (system(\"pause\")) and the XTrap init check are dropped: no console and no XTrap on this platform.",
"test": "project/python_host_test.gd (godot --headless --path project --script python_host_test.gd) and the same check through MT_TEST_MODE=python in project/client_main.gd -> system.py stops at ImportError: No module named app, the same point as ctest port.python_launcher"
},
"Main": {
"status": "TODO"
},
"CreateMetin2GameMutex": {
"status": "TODO"
},
"DestroyMetin2GameMutex": {
"status": "TODO"
},
"__ErrorPythonLibraryIsNotExist": {
"status": "TODO"
},
"__IsTimeStampOption": {
"status": "TODO"
},
"__PrintTimeStamp": {
"status": "TODO"
},
"__IsLocaleOption": {
"status": "TODO"
},
"__IsLocaleVersion": {
"status": "TODO"
},
"__IsOpenIDAuthKeyOption": {
"status": "TODO"
},
"__IsOpenIDTestOption": {
"status": "TODO"
},
"WinMain": {
"status": "TODO"
},
"GrannyError": {
"status": "TODO"
},
"Setup": {
"status": "TODO"
}
}
}