-
Notifications
You must be signed in to change notification settings - Fork 72
/
LuaPanda.lua
3205 lines (2943 loc) · 127 KB
/
LuaPanda.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--[[
Tencent is pleased to support the open source community by making LuaPanda available.
Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://opensource.org/licenses/BSD-3-Clause
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
API:
LuaPanda.printToVSCode(logStr, printLevel, type)
打印日志到VSCode Output下Debugger/log中
@printLevel: debug(0)/info(1)/error(2) 这里的日志等级需高于launch.json中配置等级日志才能输出 (可选参数,默认0)
@type: 0:VSCode output console 1:VSCode tip (可选参数,默认0)
LuaPanda.BP()
强制打断点,可以在协程中使用。建议使用以下写法:
local ret = LuaPanda and LuaPanda.BP and LuaPanda.BP();
如果成功加入断点ret返回true,否则是nil
LuaPanda.getInfo()
返回获取调试器信息。包括版本号,是否使用lib库,系统是否支持loadstring(load方法)。返回值类型string, 推荐在调试控制台中使用。
LuaPanda.doctor()
返回对当前环境的诊断信息,提示可能存在的问题。返回值类型string, 推荐在调试控制台中使用。
LuaPanda.getCWD()
用户可以调用或在调试控制台中输出这个函数,返回帮助设置CWD的路径。比如
cwd: F:/1/2/3/4/5
getinfo: @../../../../../unreal_10/slua-unreal_1018/Content//Lua/TestArray.lua
format: f:/unreal_10/slua-unreal_1018/Content/Lua/TestArray.lua
cwd是vscode传来的配置路径。getinfo是通过getinfo获取到的正在运行的文件路径。format是经过 cwd + getinfo 整合后的格式化路径。
format是传给VSCode的最终路径。
如果format路径和文件真实路径不符,导致VSCode找不到文件,通过调整工程中launch.json的cwd,使format路径和真实路径一致。
返回值类型string, 推荐在调试控制台中使用。
LuaPanda.getBreaks()
获取断点信息,返回值类型string, 推荐在调试控制台中使用。
LuaPanda.serializeTable(table)
把table序列化为字符串,返回值类型是string。
]]
--用户设置项
local openAttachMode = true; --是否开启attach模式。attach模式开启后可以在任意时刻启动vscode连接调试。缺点是没有连接调试时也会略降低lua执行效率(会不断进行attach请求)
local attachInterval = 1; --attach间隔时间(s)
local customGetSocketInstance = nil; --支持用户实现一个自定义调用luasocket的函数,函数返回值必须是一个socket实例。例: function() return require("socket.core").tcp() end;
local consoleLogLevel = 2; --打印在控制台(print)的日志等级 0 : all/ 1: info/ 2: error.
local connectTimeoutSec = 0.005; --等待连接超时时间, 单位s. 时间过长等待attach时会造成卡顿,时间过短可能无法连接。建议值0.005 - 0.05
--用户设置项END
local debuggerVer = "3.1.0"; --debugger版本号
LuaPanda = {};
local this = LuaPanda;
local tools = {}; --引用的开源工具,包括json解析和table展开工具等
this.tools = tools;
this.curStackId = 0;
--json处理
local json;
--hook状态列表
local hookState = {
DISCONNECT_HOOK = 0, --断开连接
LITE_HOOK = 1, --全局无断点
MID_HOOK = 2, --全局有断点,本文件无断点
ALL_HOOK = 3, --本文件有断点
};
--运行状态列表
local runState = {
DISCONNECT = 0, --未连接
WAIT_CMD = 1, --已连接,等待命令
STOP_ON_ENTRY = 2, --初始状态
RUN = 3,
STEPOVER = 4,
STEPIN = 5,
STEPOUT = 6,
STEPOVER_STOP = 7,
STEPIN_STOP = 8,
STEPOUT_STOP = 9,
HIT_BREAKPOINT = 10
};
local TCPSplitChar = "|*|"; --json协议分隔符,请不要修改
local MAX_TIMEOUT_SEC = 3600 * 24; --网络最大超时等待时间
--当前运行状态
local currentRunState;
local currentHookState;
--断点信息
local breaks = {}; --保存断点的数组
this.breaks = breaks; --供hookLib调用
local recCallbackId = "";
--VSCode端传过来的配置,在VSCode端的launch配置,传过来并赋值
local luaFileExtension = ""; --脚本后缀
local cwd = ""; --工作路径
local DebuggerFileName = ""; --Debugger文件名(原始,未经path处理), 函数中会自动获取
local DebuggerToolsName = "";
local lastRunFunction = {}; --上一个执行过的函数。在有些复杂场景下(find,getcomponent)一行会挺两次
local currentCallStack = {}; --获取当前调用堆栈信息
local hitBP = false; --BP()中的强制断点命中标记
local TempFilePath_luaString = ""; --VSCode端配置的临时文件存放路径
local connectHost; --记录连接端IP
local connectPort; --记录连接端口号
local sock; --tcp socket
local OSType; --VSCode识别出的系统类型,也可以自行设置。Windows_NT | Linux | Darwin
local clibPath; --chook库在VScode端的路径,也可自行设置。
local hookLib; --chook库的引用实例
local adapterVer; --VScode传来的adapter版本号
--标记位
local logLevel = 1; --日志等级all/info/error. 此设置对应的是VSCode端设置的日志等级.
local variableRefIdx = 1; --变量索引
local variableRefTab = {}; --变量记录table
local lastRunFilePath = ""; --最后执行的文件路径
local pathCaseSensitivity = true; --路径是否发大小写敏感,这个选项接收VScode设置,请勿在此处更改
local recvMsgQueue = {}; --接收的消息队列
local coroutinePool = {}; --保存用户协程的队列
local winDiskSymbolUpper = false;--设置win下盘符的大小写。以此确保从VSCode中传入的断点路径,cwd和从lua虚拟机获得的文件路径盘符大小写一致
local isNeedB64EncodeStr = false;-- 记录是否使用base64编码字符串
local loadclibErrReason = 'launch.json文件的配置项useCHook被设置为false.';
local OSTypeErrTip = "";
local pathErrTip = ""
local winDiskSymbolTip = "";
local isAbsolutePath = false;
local stopOnEntry; --用户在VSCode端设置的是否打开stopOnEntry
local userSetUseClib; --用户在VSCode端设置的是否是用clib库
local autoPathMode = false;
--Step控制标记位
local stepOverCounter = 0; --STEPOVER over计数器
local stepOutCounter = 0; --STEPOVER out计数器
local HOOK_LEVEL = 3; --调用栈偏移量,使用clib时为3,lua中不再使用此变量,而是通过函数getSpecificFunctionStackLevel获取
local isUseLoadstring = 0;
local debugger_loadString;
--临时变量
local coroutineCreate; --用来记录lua原始的coroutine.create函数
local stopConnectTime = 0; --用来临时记录stop断开连接的时间
local isInMainThread;
local receiveMsgTimer = 0;
local formatPathCache = {}; -- getinfo -> format
local isUserSetClibPath = false; --用户是否在本文件中自设了clib路径
--5.1/5.3兼容
if _VERSION == "Lua 5.1" then
debugger_loadString = loadstring;
else
debugger_loadString = load;
end
--用户在控制台输入信息的环境变量
local env = setmetatable({ }, {
__index = function( _ , varName )
local ret = this.getWatchedVariable( varName, _G.LuaPanda.curStackId , false);
return ret;
end,
__newindex = function( _ , varName, newValue )
this.setVariableValue( varName, _G.LuaPanda.curStackId, newValue);
end
});
-----------------------------------------------------------------------------
-- 流程
-----------------------------------------------------------------------------
-- 启动调试器
-- @host adapter端ip, 默认127.0.0.1
-- @port adapter端port ,默认8818
function this.start(host, port)
host = tostring(host or "127.0.0.1") ;
port = tonumber(port) or 8818;
this.printToConsole("Debugger start. connect host:" .. host .. " port:".. tostring(port), 1);
if sock ~= nil then
this.printToConsole("[Warning] 调试器已经启动,请不要再次调用start()" , 1);
return;
end
--尝试初次连接
this.changeRunState(runState.DISCONNECT);
if not this.reGetSock() then
this.printToConsole("[Error] Start debugger but get Socket fail , please install luasocket!", 2);
return;
end
connectHost = host;
connectPort = port;
local sockSuccess = sock and sock:connect(connectHost, connectPort);
if sockSuccess ~= nil then
this.printToConsole("first connect success!");
this.connectSuccess();
else
this.printToConsole("first connect failed!");
this.changeHookState(hookState.DISCONNECT_HOOK);
end
end
-- 连接成功,开始初始化
function this.connectSuccess()
this.changeRunState(runState.WAIT_CMD);
this.printToConsole("connectSuccess", 1);
--设置初始状态
local ret = this.debugger_wait_msg();
--获取debugger文件路径
if DebuggerFileName == "" then
local info = debug.getinfo(1, "S")
for k,v in pairs(info) do
if k == "source" then
DebuggerFileName = v;
this.printToVSCode("DebuggerFileName:" .. tostring(DebuggerFileName));
if hookLib ~= nil then
hookLib.sync_debugger_path(DebuggerFileName);
end
end
end
end
if DebuggerToolsName == "" then
DebuggerToolsName = tools.getFileSource();
if hookLib ~= nil then
hookLib.sync_tools_path(DebuggerToolsName);
end
end
if ret == false then
this.printToVSCode("[debugger error]初始化未完成, 建立连接但接收初始化消息失败。请更换端口重试", 2);
return;
end
this.printToVSCode("debugger init success", 1);
this.changeHookState(hookState.ALL_HOOK);
if hookLib == nil then
--协程调试
if coroutineCreate == nil and type(coroutine.create) == "function" then
this.printToConsole("change coroutine.create");
coroutineCreate = coroutine.create;
coroutine.create = function(...)
local co = coroutineCreate(...)
table.insert(coroutinePool, co);
--运行状态下,创建协程即启动hook
this.changeCoroutineHookState();
return co;
end
else
this.printToConsole("restart coroutine");
this.changeCoroutineHookState();
end
end
end
--重置数据
function this.clearData()
OSType = nil;
clibPath = nil;
-- reset breaks
breaks = {};
formatPathCache = {};
this.breaks = breaks;
if hookLib ~= nil then
hookLib.sync_breakpoints(); --清空断点信息
hookLib.clear_pathcache(); --清空路径缓存
end
end
--断开连接
function this.disconnect()
this.printToConsole("Debugger disconnect", 1);
this.clearData()
this.changeHookState( hookState.DISCONNECT_HOOK );
stopConnectTime = os.time();
this.changeRunState(runState.DISCONNECT);
if sock ~= nil then
sock:close();
end
if connectPort == nil or connectHost == nil then
--异常情况处理, 在调用LuaPanda.start()前首先调用了LuaPanda.disconnect()
this.printToConsole("[Warning] User call LuaPanda.disconnect() before set debug ip & port, please call LuaPanda.start() first!", 2);
return;
end
this.reGetSock();
end
-----------------------------------------------------------------------------
-- 调试器通用方法
-----------------------------------------------------------------------------
-- 返回断点信息
function this.getBreaks()
return breaks;
end
-- 返回路径相关信息
-- cwd:配置的工程路径 | info["source"]:通过 debug.getinfo 获得执行文件的路径 | format:格式化后的文件路径
function this.getCWD()
local ly = this.getSpecificFunctionStackLevel(lastRunFunction.func);
if type(ly) ~= "number" then
ly = 2;
end
local runSource = lastRunFunction["source"];
if runSource == nil and hookLib ~= nil then
runSource = this.getPath(tostring(hookLib.get_last_source()));
end
local info = debug.getinfo(ly, "S");
return "cwd: "..cwd .."\ngetinfo: ".. info["source"] .. "\nformat: " .. tostring(runSource) ;
end
--返回版本号等配置
function this.getBaseInfo()
local strTable = {};
local jitVer = "";
if jit and jit.version then
jitVer = "," .. tostring(jit.version);
end
strTable[#strTable + 1] = "Lua Ver:" .. _VERSION .. jitVer .." | adapterVer:" .. tostring(adapterVer) .. " | Debugger Ver:" .. tostring(debuggerVer);
local moreInfoStr = "";
if hookLib ~= nil then
local clibVer, forluaVer = hookLib.sync_getLibVersion();
local clibStr = forluaVer ~= nil and tostring(clibVer) .. " for " .. tostring(math.ceil(forluaVer)) or tostring(clibVer);
strTable[#strTable + 1] = " | hookLib Ver:" .. clibStr;
moreInfoStr = moreInfoStr .. "说明: 已加载 libpdebug 库.";
else
moreInfoStr = moreInfoStr .. "说明: 未能加载 libpdebug 库。原因请使用 LuaPanda.doctor() 查看";
end
local outputIsUseLoadstring = false
if type(isUseLoadstring) == "number" and isUseLoadstring == 1 then
outputIsUseLoadstring = true;
end
strTable[#strTable + 1] = " | supportREPL:".. tostring(outputIsUseLoadstring);
strTable[#strTable + 1] = " | useBase64EncodeString:".. tostring(isNeedB64EncodeStr);
strTable[#strTable + 1] = " | codeEnv:" .. tostring(OSType) .. '\n';
strTable[#strTable + 1] = moreInfoStr;
if OSTypeErrTip ~= nil and OSTypeErrTip ~= '' then
strTable[#strTable + 1] = '\n' ..OSTypeErrTip;
end
return table.concat(strTable);
end
--自动诊断当前环境的错误,并输出信息
function this.doctor()
local strTable = {};
if debuggerVer ~= adapterVer then
strTable[#strTable + 1] = "\n- 建议更新版本\nLuaPanda VSCode插件版本是" .. adapterVer .. ", LuaPanda.lua文件版本是" .. debuggerVer .. "。建议检查并更新到最新版本。";
strTable[#strTable + 1] = "\n更新方式 : https://github.com/Tencent/LuaPanda/blob/master/Docs/Manual/update.md";
strTable[#strTable + 1] = "\nRelease版本: https://github.com/Tencent/LuaPanda/releases";
end
--plibdebug
if hookLib == nil then
strTable[#strTable + 1] = "\n\n- libpdebug 库没有加载\n";
if userSetUseClib then
--用户允许使用clib插件
if isUserSetClibPath == true then
--用户自设了clib地址
strTable[#strTable + 1] = "用户使用 LuaPanda.lua 中 clibPath 变量指定了 plibdebug 的位置: " .. clibPath;
if this.tryRequireClib("libpdebug", clibPath) then
strTable[#strTable + 1] = "\n引用成功";
else
strTable[#strTable + 1] = "\n引用错误:" .. loadclibErrReason;
end
else
--使用默认clib地址
local clibExt, platform;
if OSType == "Darwin" then clibExt = "/?.so;"; platform = "mac";
elseif OSType == "Linux" then clibExt = "/?.so;"; platform = "linux";
else clibExt = "/?.dll;"; platform = "win"; end
local lua_ver;
if _VERSION == "Lua 5.1" then
lua_ver = "501";
else
lua_ver = "503";
end
local x86Path = clibPath .. platform .."/x86/".. lua_ver .. clibExt;
local x64Path = clibPath .. platform .."/x86_64/".. lua_ver .. clibExt;
strTable[#strTable + 1] = "尝试引用x64库: ".. x64Path;
if this.tryRequireClib("libpdebug", x64Path) then
strTable[#strTable + 1] = "\n引用成功";
else
strTable[#strTable + 1] = "\n引用错误:" .. loadclibErrReason;
strTable[#strTable + 1] = "\n尝试引用x86库: ".. x86Path;
if this.tryRequireClib("libpdebug", x86Path) then
strTable[#strTable + 1] = "\n引用成功";
else
strTable[#strTable + 1] = "\n引用错误:" .. loadclibErrReason;
end
end
end
else
strTable[#strTable + 1] = "原因是" .. loadclibErrReason;
end
end
--path
--尝试直接读当前getinfo指向的文件,看能否找到。如果能,提示正确,如果找不到,给出提示,建议玩家在这个文件中打一个断点
--检查断点,文件和当前文件的不同,给出建议
local runSource = lastRunFilePath;
if hookLib ~= nil then
runSource = this.getPath(tostring(hookLib.get_last_source()));
end
-- 在精确路径模式下的路径错误检测
if not autoPathMode and runSource and runSource ~= "" then
-- 读文件
local isFileExist = this.fileExists(runSource);
if not isFileExist then
strTable[#strTable + 1] = "\n\n- 路径存在问题\n";
--解析路径,得到文件名,到断点路径中查这个文件名
local pathArray = this.stringSplit(runSource, '/');
--如果pathArray和断点能匹配上
local fileMatch= false;
for key, _ in pairs(this.getBreaks()) do
if string.find(key, pathArray[#pathArray], 1, true) then
--和断点匹配了
fileMatch = true;
-- retStr = retStr .. "\n请对比如下路径:\n";
strTable[#strTable + 1] = this.getCWD();
strTable[#strTable + 1] = "\nfilepath: " .. key;
if isAbsolutePath then
strTable[#strTable + 1] = "\n说明:从lua虚拟机获取到的是绝对路径,format使用getinfo路径。";
else
strTable[#strTable + 1] = "\n说明:从lua虚拟机获取到的是相对路径,调试器运行依赖的绝对路径(format)是来源于cwd+getinfo拼接。";
end
strTable[#strTable + 1] = "\nfilepath是VSCode通过获取到的文件正确路径 , 对比format和filepath,调整launch.json中CWD,或改变VSCode打开文件夹的位置。使format和filepath一致即可。\n如果format和filepath路径仅大小写不一致,设置launch.json中 pathCaseSensitivity:false 可忽略路径大小写";
end
end
if fileMatch == false then
--未能和断点匹配
strTable[#strTable + 1] = "\n找不到文件:" .. runSource .. ", 请检查路径是否正确。\n或者在VSCode文件" .. pathArray[#pathArray] .. "中打一个断点后,再执行一次doctor命令,查看路径分析结果。";
end
end
end
--日志等级对性能的影响
if logLevel < 1 or consoleLogLevel < 1 then
strTable[#strTable + 1] = "\n\n- 日志等级\n";
if logLevel < 1 then
strTable[#strTable + 1] = "当前日志等级是" .. logLevel .. ", 会产生大量日志,降低调试速度。建议调整launch.json中logLevel:1";
end
if consoleLogLevel < 1 then
strTable[#strTable + 1] = "当前console日志等级是" .. consoleLogLevel .. ", 过低的日志等级会降低调试速度,建议调整LuaPanda.lua文件头部consoleLogLevel=2";
end
end
if #strTable == 0 then
strTable[#strTable + 1] = "未检测出问题";
end
return table.concat(strTable);
end
function this.fileExists(path)
local f=io.open(path,"r");
if f~= nil then io.close(f) return true else return false end
end
--返回一些信息,帮助用户定位问题
function this.getInfo()
--用户设置项
local strTable = {};
strTable[#strTable + 1] = "\n- Base Info: \n";
strTable[#strTable + 1] = this.getBaseInfo();
--已经加载C库,x86/64 未能加载,原因
strTable[#strTable + 1] = "\n\n- User Setting: \n";
strTable[#strTable + 1] = "stopOnEntry:" .. tostring(stopOnEntry) .. ' | ';
-- strTable[#strTable + 1] = "luaFileExtension:" .. luaFileExtension .. ' | ';
strTable[#strTable + 1] = "logLevel:" .. logLevel .. ' | ' ;
strTable[#strTable + 1] = "consoleLogLevel:" .. consoleLogLevel .. ' | ';
strTable[#strTable + 1] = "pathCaseSensitivity:" .. tostring(pathCaseSensitivity) .. ' | ';
strTable[#strTable + 1] = "attachMode:".. tostring(openAttachMode).. ' | ';
strTable[#strTable + 1] = "autoPathMode:".. tostring(autoPathMode).. ' | ';
if userSetUseClib then
strTable[#strTable + 1] = "useCHook:true";
else
strTable[#strTable + 1] = "useCHook:false";
end
if logLevel == 0 or consoleLogLevel == 0 then
strTable[#strTable + 1] = "\n说明:日志等级过低,会影响执行效率。请调整logLevel和consoleLogLevel值 >= 1";
end
strTable[#strTable + 1] = "\n\n- Path Info: \n";
strTable[#strTable + 1] = "clibPath: " .. tostring(clibPath) .. '\n';
strTable[#strTable + 1] = "debugger: " .. this.getPath(DebuggerFileName) .. '\n';
strTable[#strTable + 1] = this.getCWD();
if not autoPathMode then
if isAbsolutePath then
strTable[#strTable + 1] = "\n说明:从lua虚拟机获取到的是绝对路径,format使用getinfo路径。" .. winDiskSymbolTip;
else
strTable[#strTable + 1] = "\n说明:从lua虚拟机获取到的路径(getinfo)是相对路径,调试器运行依赖的绝对路径(format)是来源于cwd+getinfo拼接。如format路径错误请尝试调整cwd或改变VSCode打开文件夹的位置。也可以在format对应的文件下打一个断点,调整直到format和Breaks Info中断点路径完全一致。" .. winDiskSymbolTip;
end
else
strTable[#strTable + 1] = "\n说明:已开启autoPathMode自动路径模式,调试器会根据getinfo获得的文件名自动查找文件位置,请确保VSCode打开的工程中不存在同名lua文件。";
end
if pathErrTip ~= nil and pathErrTip ~= '' then
strTable[#strTable + 1] = '\n' .. pathErrTip;
end
strTable[#strTable + 1] = "\n\n- Breaks Info: \n";
strTable[#strTable + 1] = this.serializeTable(this.getBreaks(), "breaks");
return table.concat(strTable);
end
--判断是否在协程中
function this.isInMain()
return isInMainThread;
end
--添加路径,尝试引用库。完成后把cpath还原,返回引用结果true/false
-- @libName 库名
-- path lib的cpath路径
function this.tryRequireClib(libName , libPath)
this.printToVSCode("tryRequireClib search : [" .. libName .. "] in "..libPath);
local savedCpath = package.cpath;
package.cpath = package.cpath .. ';' .. libPath;
this.printToVSCode("package.cpath:" .. package.cpath);
local status, err = pcall(function() hookLib = require(libName) end);
if status then
if type(hookLib) == "table" and this.getTableMemberNum(hookLib) > 0 then
this.printToVSCode("tryRequireClib success : [" .. libName .. "] in "..libPath);
package.cpath = savedCpath;
return true;
else
loadclibErrReason = "tryRequireClib fail : require success, but member function num <= 0; [" .. libName .. "] in "..libPath;
this.printToVSCode(loadclibErrReason);
hookLib = nil;
package.cpath = savedCpath;
return false;
end
else
-- 此处考虑到tryRequireClib会被调用两次,日志级别设置为0,防止输出不必要的信息。
loadclibErrReason = err;
this.printToVSCode("[Require clib error]: " .. err, 0);
end
package.cpath = savedCpath;
return false
end
------------------------字符串处理-------------------------
-- 倒序查找字符串 a.b/c查找/ , 返回4
-- @str 被查找的长串
-- @subPattern 查找的子串, 也可以是pattern
-- @plain plane text / pattern
-- @return 未找到目标串返回nil. 否则返回倒序找到的字串位置
function this.revFindString(str, subPattern, plain)
local revStr = string.reverse(str);
local _, idx = string.find(revStr, subPattern, 1, plain);
if idx == nil then return nil end;
return string.len(revStr) - idx + 1;
end
-- 反序裁剪字符串 如:print(subString("a.b/c", "/"))输出c
-- @return 未找到目标串返回nil. 否则返回被裁剪后的字符串
function this.revSubString(str, subStr, plain)
local idx = this.revFindString(str, subStr, plain)
if idx == nil then return nil end;
return string.sub(str, idx + 1, str.length)
end
-- 把字符串按reps分割成并放入table
-- @str 目标串
-- @reps 分割符。注意这个分隔符是一个pattern
function this.stringSplit( str, separator )
local retStrTable = {}
string.gsub(str, '[^' .. separator ..']+', function ( word )
table.insert(retStrTable, word)
end)
return retStrTable;
end
-- 保存CallbackId(通信序列号)
function this.setCallbackId( id )
if id ~= nil and id ~= "0" then
recCallbackId = tostring(id);
end
end
-- 读取CallbackId(通信序列号)。读取后记录值将被置空
function this.getCallbackId()
if recCallbackId == nil then
recCallbackId = "0";
end
local id = recCallbackId;
recCallbackId = "0";
return id;
end
-- reference from https://www.lua.org/pil/20.1.html
function this.trim (s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
--返回table中成员数量(数字key和非数字key之和)
-- @t 目标table
-- @return 元素数量
function this.getTableMemberNum(t)
local retNum = 0;
if type(t) ~= "table" then
this.printToVSCode("[debugger Error] getTableMemberNum get "..tostring(type(t)), 2)
return retNum;
end
for k,v in pairs(t) do
retNum = retNum + 1;
end
return retNum;
end
-- 生成一个消息Table
function this.getMsgTable(cmd ,callbackId)
callbackId = callbackId or 0;
local msgTable = {};
msgTable["cmd"] = cmd;
msgTable["callbackId"] = callbackId;
msgTable["info"] = {};
return msgTable;
end
function this.serializeTable(tab, name)
local sTable = tools.serializeTable(tab, name);
return sTable;
end
------------------------日志打印相关-------------------------
-- 把日志打印在VSCode端
-- @str: 日志内容
-- @printLevel: all(0)/info(1)/error(2)
-- @type: 0:vscode console 1:vscode tip
function this.printToVSCode(str, printLevel, type)
type = type or 0;
printLevel = printLevel or 0;
if currentRunState == runState.DISCONNECT or logLevel > printLevel then
return;
end
local sendTab = {};
sendTab["callbackId"] = "0";
if type == 0 then
sendTab["cmd"] = "log";
else
sendTab["cmd"] = "tip";
end
sendTab["info"] = {};
sendTab["info"]["logInfo"] = tostring(str);
this.sendMsg(sendTab);
end
-- 把日志打印在控制台
-- @str: 日志内容
-- @printLevel: all(0)/info(1)/error(2)
function this.printToConsole(str, printLevel)
printLevel = printLevel or 0;
if consoleLogLevel > printLevel then
return;
end
print("[LuaPanda] ".. tostring(str));
end
-----------------------------------------------------------------------------
-- 提升兼容性方法
-----------------------------------------------------------------------------
--生成平台无关的路径。
--return:nil(error)/path
function this.genUnifiedPath(path)
if path == "" or path == nil then
return "";
end
--大小写不敏感时,路径全部转为小写
if pathCaseSensitivity == false then
path = string.lower(path);
end
--统一路径全部替换成/
path = string.gsub(path, [[\]], "/");
--处理 /../ /./
local pathTab = this.stringSplit(path, '/');
local newPathTab = {};
for k, v in ipairs(pathTab) do
if v == '.' then
--continue
elseif v == ".." and #newPathTab >= 1 and newPathTab[#newPathTab]:sub(2,2) ~= ':' then
--newPathTab有元素,最后一项不是X:
table.remove(newPathTab);
else
table.insert(newPathTab, v);
end
end
--重新拼合后如果是mac路径第一位是/
local newpath = table.concat(newPathTab, '/');
if path:sub(1,1) == '/' then
newpath = '/'.. newpath;
end
--win下按照winDiskSymbolUpper的设置修改盘符大小
if "Windows_NT" == OSType then
if winDiskSymbolUpper then
newpath = newpath:gsub("^%a:", string.upper);
winDiskSymbolTip = "路径中Windows盘符已转为大写。"
else
newpath = newpath:gsub("^%a:", string.lower);
winDiskSymbolTip = "路径中Windows盘符已转为小写。"
end
end
return newpath;
end
function this.getCacheFormatPath(source)
if source == nil then return formatPathCache end;
return formatPathCache[source];
end
function this.setCacheFormatPath(source, dest)
formatPathCache[source] = dest;
end
-----------------------------------------------------------------------------
-- 内存相关
-----------------------------------------------------------------------------
function this.sendLuaMemory()
local luaMem = collectgarbage("count");
local sendTab = {};
sendTab["callbackId"] = "0";
sendTab["cmd"] = "refreshLuaMemory";
sendTab["info"] = {};
sendTab["info"]["memInfo"] = tostring(luaMem);
this.sendMsg(sendTab);
end
-----------------------------------------------------------------------------
-- 网络相关方法
-----------------------------------------------------------------------------
-- 刷新socket
-- @return true/false 刷新成功/失败
function this.reGetSock()
if sock ~= nil then
pcall(function() sock:close() end);
end
--call ue4 luasocket
sock = lua_extension and lua_extension.luasocket and lua_extension.luasocket().tcp();
if sock == nil then
--call u3d luasocket
if pcall(function() sock = require("socket.core").tcp(); end) then
this.printToConsole("reGetSock success");
sock:settimeout(connectTimeoutSec);
else
--call custom function to get socket
if customGetSocketInstance and pcall( function() sock = customGetSocketInstance(); end ) then
this.printToConsole("reGetSock custom success");
sock:settimeout(connectTimeoutSec);
else
this.printToConsole("[Error] reGetSock fail", 2);
return false;
end
end
else
--set ue4 luasocket
this.printToConsole("reGetSock ue4 success");
sock:settimeout(connectTimeoutSec);
end
return true;
end
-- 定时(以函数return为时机) 进行attach连接
function this.reConnect()
if currentHookState == hookState.DISCONNECT_HOOK then
if os.time() - stopConnectTime < attachInterval then
this.printToConsole("Reconnect time less than 1s");
this.printToConsole("os.time:".. os.time() .. " | stopConnectTime:" ..stopConnectTime);
return 1;
end
if sock == nil then
this.reGetSock();
end
local sockSuccess, status = sock:connect(connectHost, connectPort);
if sockSuccess == 1 or status == "already connected" then
this.printToConsole("reconnect success");
this.connectSuccess();
else
this.printToConsole("reconnect failed . retCode:" .. tostring(sockSuccess) .. " status:" .. status);
stopConnectTime = os.time();
end
return 1;
end
return 0;
end
-- 向adapter发消息
-- @sendTab 消息体table
function this.sendMsg( sendTab )
if isNeedB64EncodeStr and sendTab["info"] ~= nil then
for _, v in ipairs(sendTab["info"]) do
if v["type"] == "string" then
v["value"] = tools.base64encode(v["value"])
end
end
end
local sendStr = json.encode(sendTab);
if currentRunState == runState.DISCONNECT then
this.printToConsole("[debugger error] disconnect but want sendMsg:" .. sendStr, 2);
this.disconnect();
return;
end
local succ,err;
if pcall(function() succ,err = sock:send(sendStr..TCPSplitChar.."\n"); end) then
if succ == nil then
if err == "closed" then
this.disconnect();
end
end
end
end
-- 处理 收到的消息
-- @dataStr 接收的消息json
function this.dataProcess( dataStr )
this.printToVSCode("debugger get:"..dataStr);
local dataTable = json.decode(dataStr);
if dataTable == nil then
this.printToVSCode("[error] Json is error", 2);
return;
end
if dataTable.callbackId ~= "0" then
this.setCallbackId(dataTable.callbackId);
end
if dataTable.cmd == "continue" then
this.changeRunState(runState.RUN);
local msgTab = this.getMsgTable("continue", this.getCallbackId());
this.sendMsg(msgTab);
elseif dataTable.cmd == "stopOnStep" then
this.changeRunState(runState.STEPOVER);
local msgTab = this.getMsgTable("stopOnStep", this.getCallbackId());
this.sendMsg(msgTab);
this.changeHookState(hookState.ALL_HOOK);
elseif dataTable.cmd == "stopOnStepIn" then
this.changeRunState(runState.STEPIN);
local msgTab = this.getMsgTable("stopOnStepIn", this.getCallbackId());
this.sendMsg(msgTab);
this.changeHookState(hookState.ALL_HOOK);
elseif dataTable.cmd == "stopOnStepOut" then
this.changeRunState(runState.STEPOUT);
local msgTab = this.getMsgTable("stopOnStepOut", this.getCallbackId());
this.sendMsg(msgTab);
this.changeHookState(hookState.ALL_HOOK);
elseif dataTable.cmd == "setBreakPoint" then
this.printToVSCode("dataTable.cmd == setBreakPoint");
local bkPath = dataTable.info.path;
bkPath = this.genUnifiedPath(bkPath);
if autoPathMode then
-- 自动路径模式下,仅保留文件名
bkPath = this.getFilenameFromPath(bkPath);
end
this.printToVSCode("setBreakPoint path:"..tostring(bkPath));
breaks[bkPath] = dataTable.info.bks;
-- 当v为空时,从断点列表中去除文件
for k, v in pairs(breaks) do
if next(v) == nil then
breaks[k] = nil;
end
end
--sync breaks to c
if hookLib ~= nil then
hookLib.sync_breakpoints();
end
if currentRunState ~= runState.WAIT_CMD then
if hookLib == nil then
local fileBP, G_BP =this.checkHasBreakpoint(lastRunFilePath);
if fileBP == false then
if G_BP == true then
this.changeHookState(hookState.MID_HOOK);
else
this.changeHookState(hookState.LITE_HOOK);
end
else
this.changeHookState(hookState.ALL_HOOK);
end
end
else
local msgTab = this.getMsgTable("setBreakPoint", this.getCallbackId());
this.sendMsg(msgTab);
return;
end
--其他时机收到breaks消息
local msgTab = this.getMsgTable("setBreakPoint", this.getCallbackId());
this.sendMsg(msgTab);
-- 打印调试信息
this.printToVSCode("LuaPanda.getInfo()\n" .. this.getInfo())
this.debugger_wait_msg();
elseif dataTable.cmd == "setVariable" then
if currentRunState == runState.STOP_ON_ENTRY or
currentRunState == runState.HIT_BREAKPOINT or
currentRunState == runState.STEPOVER_STOP or
currentRunState == runState.STEPIN_STOP or
currentRunState == runState.STEPOUT_STOP then
local msgTab = this.getMsgTable("setVariable", this.getCallbackId());
local varRefNum = tonumber(dataTable.info.varRef);
local newValue = tostring(dataTable.info.newValue);
local needFindVariable = true; --如果变量是基础类型,直接赋值,needFindVariable = false; 如果变量是引用类型,needFindVariable = true
local varName = tostring(dataTable.info.varName);
-- 根据首末含有" ' 判断 newValue 是否是字符串
local first_chr = string.sub(newValue, 1, 1);
local end_chr = string.sub(newValue, -1, -1);
if first_chr == end_chr then
if first_chr == "'" or first_chr == '"' then
newValue = string.sub(newValue, 2, -2);
needFindVariable = false;
end
end
--数字,nil,false,true的处理
if newValue == "nil" and needFindVariable == true then newValue = nil; needFindVariable = false;
elseif newValue == "true" and needFindVariable == true then newValue = true; needFindVariable = false;
elseif newValue == "false" and needFindVariable == true then newValue = false; needFindVariable = false;
elseif tonumber(newValue) and needFindVariable == true then newValue = tonumber(newValue); needFindVariable = false;
end
-- 如果新值是基础类型,则不需遍历
if dataTable.info.stackId ~= nil and tonumber(dataTable.info.stackId) ~= nil and tonumber(dataTable.info.stackId) > 1 then
this.curStackId = tonumber(dataTable.info.stackId);
else
this.printToVSCode("未能获取到堆栈层级,默认使用 this.curStackId;")
end
if varRefNum < 10000 then
-- 如果修改的是一个 引用变量,那么可直接赋值。但还是要走变量查询过程。查找和赋值过程都需要steakId。 目前给引用变量赋值Object,steak可能有问题
msgTab.info = this.createSetValueRetTable(varName, newValue, needFindVariable, this.curStackId, variableRefTab[varRefNum]);
else
-- 如果修改的是一个基础类型
local setLimit; --设置检索变量的限定区域
if varRefNum >= 10000 and varRefNum < 20000 then setLimit = "local";
elseif varRefNum >= 20000 and varRefNum < 30000 then setLimit = "global";
elseif varRefNum >= 30000 then setLimit = "upvalue";
end
msgTab.info = this.createSetValueRetTable(varName, newValue, needFindVariable, this.curStackId, nil, setLimit);
end
this.sendMsg(msgTab);
this.debugger_wait_msg();
end
elseif dataTable.cmd == "getVariable" then
--仅在停止时处理消息,其他时刻收到此消息,丢弃
if currentRunState == runState.STOP_ON_ENTRY or
currentRunState == runState.HIT_BREAKPOINT or
currentRunState == runState.STEPOVER_STOP or
currentRunState == runState.STEPIN_STOP or
currentRunState == runState.STEPOUT_STOP then
--发送变量给游戏,并保持之前的状态,等待再次接收数据
--dataTable.info.varRef 10000~20000局部变量
-- 20000~30000全局变量
-- 30000~ upvalue
-- 1000~2000局部变量的查询,2000~3000全局,3000~4000upvalue
local msgTab = this.getMsgTable("getVariable", this.getCallbackId());
local varRefNum = tonumber(dataTable.info.varRef);
if varRefNum < 10000 then
--查询变量, 此时忽略 stackId
local varTable = this.getVariableRef(dataTable.info.varRef, true);
msgTab.info = varTable;
elseif varRefNum >= 10000 and varRefNum < 20000 then
--局部变量
if dataTable.info.stackId ~= nil and tonumber(dataTable.info.stackId) > 1 then
this.curStackId = tonumber(dataTable.info.stackId);
if type(currentCallStack[this.curStackId - 1]) ~= "table" or type(currentCallStack[this.curStackId - 1].func) ~= "function" then
local str = "getVariable getLocal currentCallStack " .. this.curStackId - 1 .. " Error\n" .. this.serializeTable(currentCallStack, "currentCallStack");
this.printToVSCode(str, 2);
msgTab.info = {};
else
local stackId = this.getSpecificFunctionStackLevel(currentCallStack[this.curStackId - 1].func); --去除偏移量
local varTable = this.getVariable(stackId, true);
msgTab.info = varTable;
end
end
elseif varRefNum >= 20000 and varRefNum < 30000 then
--全局变量
local varTable = this.getGlobalVariable();
msgTab.info = varTable;
elseif varRefNum >= 30000 then
--upValue
if dataTable.info.stackId ~= nil and tonumber(dataTable.info.stackId) > 1 then
this.curStackId = tonumber(dataTable.info.stackId);
if type(currentCallStack[this.curStackId - 1]) ~= "table" or type(currentCallStack[this.curStackId - 1].func) ~= "function" then
local str = "getVariable getUpvalue currentCallStack " .. this.curStackId - 1 .. " Error\n" .. this.serializeTable(currentCallStack, "currentCallStack");
this.printToVSCode(str, 2);
msgTab.info = {};
else
local varTable = this.getUpValueVariable(currentCallStack[this.curStackId - 1 ].func, true);
msgTab.info = varTable;
end
end
end
this.sendMsg(msgTab);
this.debugger_wait_msg();
end