该版本增加了远程调试功能,在根目录中新建了pydebugger目录,在该目录内新增了TCPServer.py的python脚本,需要通过python3来运行本脚本。该脚本在运行时,默认会监听9999端口...

    页面导航: 项目下载地址:

    zenglServer源代码的相关地址:https://github.com/zenglong/zenglServer  当前版本对应的tag标签为:v0.9.0

zenglServer v0.9.0:

    该版本增加了远程调试功能,在根目录中新建了pydebugger目录,在该目录内新增了TCPServer.py的python脚本,需要通过python3来运行本脚本。

    该脚本在运行时,默认会监听9999端口(可以给脚本传递参数来改变绑定的端口号),当python脚本接收到zenglServer的调试连接时,就会等待用户输入调试命令,并将这些命令发送给zenglServer,再由zenglServer执行调试命令和返回调试结果,最后python会将结果显示到用户终端上:

[email protected]:~/zenglServer$ python3 pydebugger/TCPServer.py
listen connection [port:9999]...
127.0.0.1 connected:
file:my_webroot/v0_8_0/test.zl,line:1,breakIndex:0
1    use builtin;

zl debug >>> h
 p 调试变量信息 usage:p express
 b 设置断点 usage:b filename lineNumber[ count] | b lineNumber[ count]
 B 查看断点列表 usage:B
 T 查看脚本函数的堆栈调用信息 usage:T
 d 删除某断点 usage:d breakIndex
 D 禁用某断点 usage:D breakIndex
 C 设置条件断点 usage:C breakIndex condition-express
 L 设置日志断点 usage:L breakIndex log-express
 N 设置断点次数 usage:N breakIndex count
 s 单步步入 usage:s
 S 单步步过 usage:S
 r 执行到返回 usage:r
 c 继续执行 usage:c
 l 显示源码 usage:l filename [lineNumber[ offset]] | l [lineNumber[ offset]]
 u 执行到指定的行 usage:u filename lineNumber | u lineNumber
 h 显示帮助信息

zl debug >>> l 9 10
current run line:1 [my_webroot/v0_8_0/test.zl]
1    use builtin;    <<<---[ current line] ***
2
3    def TRUE 1;
4    def FALSE 0;
5    def MD5_LOWER_CASE 1;
6    def MD5_UPPER_CASE 0;
7    def MD5_32BIT 1;
8    def MD5_16BIT 0;
9
10    print '<!Doctype html>
11    <html>
12    <head><meta http-equiv="content-type" content="text/html;charset=utf-8" />
13    <title>json编解码测试</title>
14    </head>
15    <body>';
16
17    json = '{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}';
18
19    json = bltJsonDecode(json);

zl debug >>> u 19
file:my_webroot/v0_8_0/test.zl,line:19,breakIndex:1
19    json = bltJsonDecode(json);

zl debug >>> p json
json :string:{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}

zl debug >>> c
listen connection...
^Cexcept...
[email protected]:~/zenglServer$

// 通过给python脚本传递参数,可以改变绑定的端口号:

[email protected]:~/zenglServer$ python3 pydebugger/TCPServer.py 8989
listen connection [port:8989]...
^Cexcept...
[email protected]:~/zenglServer$ 


    此外,在根目录的config.zl中增加了和远程调试相关的配置:

def TRUE 1;
def FALSE 0;

................................................

remote_debug_enable = FALSE; // 是否开启远程调试,默认为FALSE即不开启,设置为TRUE可以开启远程调试
remote_debugger_ip = '127.0.0.1'; // 远程调试器的ip地址
remote_debugger_port = 9999; // 远程调试器的端口号


    通过将remote_debug_enable设置为TRUE即可开启远程调试,如果开启了远程调试,那么当运行zengl脚本时,zenglServer就会根据remote_debugger_ip(IP地址)和remote_debugger_port(端口号)去连接远程调试器(目前远程调试器是使用python编写的,即上面提到的TCPServer.py)。连接上调试器后,调试器会接收用户输入的调试命令,并将命令发给zenglServer,由zenglServer执行命令并返回结果。

    进行远程调试时,zenglServer的logfile日志文件中也会记录下用户输入的调试命令:

[email protected]:~/zenglServer$ tail -f logfile
webroot: my_webroot
session_dir: my_sessions session_expire: 1440 cleaner_interval: 3600
remote_debug_enable: True remote_debugger_ip: 127.0.0.1 remote_debugger_port: 9999
bind done
.................................................
-----------------------------------
Thu Mar  1 09:21:33 2018
recv [client_socket_fd:9] [lst_idx:0] [pid:4664] [tid:4667]:

request header: Host: 127.0.0.1:8083 | User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 | Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 | Accept-Encoding: gzip, deflate | Cookie: __uvt=; uvts=6l2bv2GQOomwZIup | Connection: keep-alive | Upgrade-Insecure-Requests: 1 | Cache-Control: max-age=0 |

url: /v0_8_0/test.zl
url_path: /v0_8_0/test.zl
full_path: my_webroot/v0_8_0/test.zl
zl debug info: Socket created [12]
zl debug info: connecting to 127.0.0.1:9999... connected
zl debug info: debugger command: l test.zl
zl debug info: debugger command: h
zl debug info: debugger command: u 19
zl debug info: debugger command: p json
zl debug info: debugger command: c
zl debug info: close socket [12]
status: 200, content length: 917
response header: HTTP/1.1 200 OK

Content-Type: text/html

Content-Length: 917

Connection: Closed

Server: zenglServer

free socket_list[0]/list_cnt:0 epoll_fd_add_count:0 pid:4664 tid:4667


TCPServer.py:

    TCPServer.py的源码如下:

# -*- coding: utf-8 -*-
# 请使用python3运行本脚本
import sys
if sys.version_info[0] < 3:
	sys.exit("Must be using Python 3")
import socketserver
import json
try:
	import readline
except ImportError:
	# 如果没有安装readline模块,则显示警告信息,readline模块主要用于linux系统中,在接受用户输入时可以使用上下左右键等
	# 如果需要安装readline模块,可以通过:https://pypi.python.org/pypi/pyreadline 下载pyreadline-2.1.zip,解压后,进入解压的目录,运行python3 setup.py install命令进行安装即可
	print("warning: your system have no readline module!")
import os
# from json.decoder import JSONDecodeError # start from python 3.5.x, so no portability

# 当接收到的数据为空时,即连接已经断开,则抛出MyEmptyException异常
class MyEmptyException(Exception):
	"""Base class for other exceptions"""
	def __init__(self, code, msg):
		super(MyEmptyException, self).__init__(code, msg)
		self.code = code

# MyTCPHandler类会在每次接收到连接时被实例化一次,并通过handle方法去处理连接
class MyTCPHandler(socketserver.BaseRequestHandler):
	"""
	The request handler class for our server.

	It is instantiated once per connection to the server, and must
	override the handle() method to implement communication to the
	client.
	"""
	dir_path = None # 当前主执行脚本的目录路径,使用l命令查看源码,以及使用b命令或者u命令时,如果要跟随文件名的话,该文件名需要相对于dir_path
	cur_filename = None # 当前执行脚本的文件名,包括目录路径在内
	cur_line = None # 当前执行代码所在的行号
	main_script_filename = None # 当前主执行脚本的文件名,包括目录路径在内
	filelist = None # 该成员用于缓存l命令获取到的脚本源码
	max_recv_bytes = 81920 # 每次从zenglServer客户端接收数据的最大字节数,如果要接收的数据比较大时,可以适当的调整该成员的值,以字节为单位
	offset = 8 # 使用l命令查看源码时,需要显示的上下偏移行数,例如:当offset为8时,显示第16行的代码,会将第8行到第24行的代码给显示出来

	# 从调试连接中获取zenglServer发送过来的数据
	def myrecv(self, bytes):
		#print('wait recv...')
		# self.request is the TCP socket connected to the client
		recv_msg = self.request.recv(bytes)
		if not recv_msg: # 如果在等待接收数据的过程中,zenglServer断开了连接,则返回None
			# EOF, client closed, just return
			print('listen connection...')
			return None
		recv_msg = recv_msg.decode('utf-8') # 将获取到的数据进行utf8解码,转为unicode字符串
		return recv_msg

	# 通过l命令从zenglServer获取到脚本的源码后,python会根据换行符将源码分割成content_list字符串列表,列表的每一项都对应一行源代码
	# 接着就可以根据line_no行号,以及offset行偏移,来从列表中将这些行的源码给获取出来了
	def get_content_ret_list(self, orig_path, normal_path, content_list, line_no = None, offset = None, show_filename = True):
		cur_normal_path = os.path.normpath(self.cur_filename)
		if(line_no is not None):
			cur_line = line_no
		else:
			cur_line = self.cur_line if(normal_path == cur_normal_path) else 1
		if(offset is None):
			offset = self.offset
		cur_index = cur_line - 1
		if (cur_index - offset) < 0:
			start_index = 0
		else:
			start_index = (cur_index - offset)
			if(start_index > (len(content_list) - 1)):
				start_index = len(content_list) - 1
		if(show_filename):
			if(normal_path == cur_normal_path):
				print("current run line:{} [{}]".format(self.cur_line, self.cur_filename))
			else:
				print("[{}]".format(orig_path))
		ret_list = content_list[start_index:cur_line+offset]
		ret_content = ""
		start_line = start_index + 1
		for line in ret_list:
			if (start_line == self.cur_line) and (normal_path == cur_normal_path):
				ret_content += "{}    {}    <<<---[ current line] ***\n".format(start_line, line) # 将当前执行代码所在的行用 <<<---[ current line] *** 在该行的末尾进行标注
			else:
				ret_content += "{}    {}\n".format(start_line, line)
			start_line += 1
		return ret_content

	# 处理用户输入的l命令,当用户通过l命令查看源码时,python会先将要查看的脚本文件名(相对于主执行脚本的文件路径)发送给zenglServer,由zenglServer将该脚本的源码一次发过来
	# 接着python会将源码根据\n换行符分割为字符串列表(列表的每一项都对应一行源码),并将该列表存储到filelist词典中,词典的key为脚本文件的常规化后的路径,从而将源码缓存起来
	# 在显示源码时,就只需根据line_no行号和offset行偏移值,从列表中将所需的源码提取出来即可
	def list_command(self, filename = None, line_no = None, offset = None, show_filename = True):
		if self.filelist is None:
			self.filelist = dict()
		if filename is None:
			filename = self.cur_filename.replace(self.dir_path, '')
		orig_path = self.dir_path + filename
		normal_path = os.path.normpath(orig_path)
		if normal_path in self.filelist:
			return self.get_content_ret_list(orig_path, normal_path, self.filelist[normal_path], line_no, offset, show_filename)
		self.request.sendall("l {}".format(filename).encode('utf-8'))
		file_content = self.myrecv(self.max_recv_bytes)
		if not file_content:
			raise MyEmptyException
		self.request.sendall("ok".encode('utf-8'))
		self.filelist[normal_path] = file_content.split("\n")
		return self.get_content_ret_list(orig_path, normal_path, self.filelist[normal_path], line_no, offset, show_filename)

	# 将用户输入的命令根据空格符进行分割,并将分割形成的列表返回
	def get_command_list(self, command):
		command_list = command.split(" ")
		command_list = [x for x in command_list if x]
		for idx, line in enumerate(command_list):
			command_list[idx] = line.strip()
		return command_list

	# handle方法用于处理调试器接收到的zenglServer连接,该方法会将用户输入的调试命令,通过连接发送给zenglServer,并将zenglServer返回的结果显示出来
	def handle(self):
		print("{} connected:".format(self.client_address[0]))
		while(True):
			recv_msg = self.myrecv(1024)
			if not recv_msg:
				return
			try:
				# 如果接收到的是json数据,则说明当前发生了中断(单步执行或者触发断点等发生的中断),json中包含了中断所在的脚本文件名,行号等信息
				recv_msg_decode = json.loads(recv_msg)
			except ValueError:
				# 如果不是json数据,则是zenglServer发来的其他的输出信息,例如日志断点中日志表达式的执行结果等,这些输出信息则直接通过print打印出来
				print("{}".format(recv_msg))
				self.request.sendall("ok".encode('utf-8')) # 接收到数据后,响应一个ok,表示接收到了数据,zenglServer的调试模块收到响应的ok才会继续执行
				continue
			if(recv_msg_decode['action'] == "debug"):
				self.main_script_filename = recv_msg_decode['main_script_filename'] # 获取主执行脚本的文件名,包括目录路径在内
				if(self.dir_path is None):
					ridx = self.main_script_filename.rfind('/')
					if(ridx >= 0):
						self.dir_path = self.main_script_filename[0:ridx+1] # 设置dir_path即主执行脚本的目录路径
					else:
						self.dir_path = ''
				self.cur_filename = recv_msg_decode['filename'] # 设置当前的执行脚本的文件名(包括目录路径在内)
				self.cur_line = int(recv_msg_decode['line']) # 设置当前执行代码所在的行
				cur_normal_path = os.path.normpath(self.cur_filename)
				main_normal_path = os.path.normpath(self.main_script_filename)
				# 将中断发生的脚本文件名(包括目录路径在内),行号,触发的断点索引,主执行脚本文件路径等打印出来
				if(cur_normal_path == main_normal_path):
					format_str = "file:{},line:{},breakIndex:{}" # 如果当前执行脚本就是主执行脚本的话,则不显示main_script和dir_path信息
				else:
					format_str = "file:{},line:{},breakIndex:{}  [main_script:{}, dir_path:{}]"
				print(format_str.format(self.cur_filename, self.cur_line, recv_msg_decode['breakIndex'], self.main_script_filename, self.dir_path))
				if type(self.filelist) is not dict or cur_normal_path not in self.filelist: # 如果没有获取过当前执行脚本的源码,则通过list_command方法从zenglServer获取源码
					self.list_command(None, self.cur_line, None, False)
				print("{}    {}\n".format(self.cur_line, self.filelist[cur_normal_path][self.cur_line-1])) # 将当前执行代码所在的行的源码显示出来
				while(True): # 循环接受用户输入的调试命令
					input_command = input('zl debug >>> ').strip()
					command_list = self.get_command_list(input_command)
					command = " ".join(command_list)
					if(command == ''):
						print('command is empty')
						continue
					elif(command_list[0] == 'l'): # l查看源码命令进行单独处理
						filename = None # 用户输入的要查看源码的脚本文件名,相对于主执行脚本的文件路径
						line_no = None # 用户输入的要查看的行号
						offset = None # 要查看的行偏移
						for idx, command_part in enumerate(command_list):
							if(idx == 1): # l命令的第一个参数如果是数字则表示行号,否则就表示文件名
								if(command_part.isdigit()):
									line_no = int(command_part)
								else:
									filename = command_part
							elif(idx == 2): # 第二个参数为行号或者行偏移
								if(filename is not None): # 如果设置过文件名,就是行号
									line_no = int(command_part)
								else: # 否则就是行偏移
									offset = int(command_part)
							elif((idx == 3) and (filename is not None)): # 如果设置过文件名,第三个参数就是行偏移
								offset = int(command_part)
						try:
							print(self.list_command(filename, line_no, offset)) # 通过list_command方法来处理l命令
							continue
						except MyEmptyException:
							return
					self.request.sendall(input_command.encode('utf-8')) # 将其他调试命令发送给zenglServer去处理
					recv_msg = self.myrecv(self.max_recv_bytes) # 等待接收zenglServer的处理结果
					if not recv_msg: # 如果zenglServer关闭了连接,则直接返回
						return
					try:
						# 如果接收到的是json数据,则通过json中的exit字段来判断是否结束当前的中断
						recv_msg_decode = json.loads(recv_msg)
						self.request.sendall("ok".encode('utf-8')) # 响应ok给zenglServer
						if(recv_msg_decode['exit'] == 1): # 如果是c命令等,设置了exit为1,就break跳出内层的循环,从而结束当前的中断,并等待接收下一次的中断信息
							break
					except ValueError:
						# 如果接收到的不是json数据,则直接将信息通过print打印出来
						print("{}".format(recv_msg))
						self.request.sendall("ok".encode('utf-8')) # 响应ok给zenglServer
			else: # 理论上暂时不会执行到这里,目前,如果传递的中断json的action不是debug,就直接将数据打印出来
				print("{}".format(recv_msg))
				self.request.sendall("ok".encode('utf-8'))

if __name__ == "__main__":
	#HOST, PORT = "localhost", 9999
	if len(sys.argv) > 1:
		PORT = int(sys.argv[1]) # 通过第一个参数可以设置需要绑定的端口号,默认为9999
	else:
		PORT = 9999
	HOST = ""

	socketserver.TCPServer.allow_reuse_address = True
	# Create the server, binding to localhost on port 9999
	server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

	print('listen connection [port:{}]...'.format(PORT))
	try:
		# Activate the server; this will keep running until you
		# interrupt the program with Ctrl-C
		server.serve_forever()
	except:
		print('except...')
		quit()


debug.c:

    zenglServer中处理调试命令相关的C源码主要位于debug.c文件中:

/*
 * debug.c
 *
 *  Created on: 2018-2-9
 *      Author: zengl
 */

..............................................................

/**
 * p命令:执行p命令后面的第一个参数对应的表达式,并将表达式的结果,以字符串的形式存储到format_send_msg动态字符串中,稍后会将其发送给远程调试器
 */
static void debug_command_print(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, char * str, int * start, int str_count, int str_size)
{
	......................................................
}

/**
 * b命令:设置断点
 * 例如:b test.zl 19 就是在test.zl脚本的第19行设置断点
 * 如果不提供脚本文件名,就是在当前执行脚本中设置断点,例如:b 19就是在当前执行脚本的第19行设置断点
 * 脚本文件名必须是相对于主执行脚本的相对路径
 * 假设主执行脚本是test.zl,并在test.zl中通过inc '../test2.zl'加载了test2.zl
 * 那么,要在test2.zl中设置断点的话,就需要使用b ../test2.zl 19这样的写法
 * 可以在b命令最后跟随一个断点次数,例如:b test.zl 19 1表示在test.zl的第19行设置断点,断点次数为1,也就是只能中断一次
 */
static int debug_command_set_breakpoint(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, char * str, int * start,
		char * cur_filename, int * arg_count, MAIN_DATA * my_data)
{
	......................................................
}

/**
 * B命令:将设置过的断点都列举出来,并将列举出来的断点列表存到format_send_msg动态字符串中,稍后会将其发送给远程调试器
 */
static void debug_command_list_breakpoints(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info,
		ZL_EXP_INT breakIndex,
		ZL_EXP_CHAR * cur_filename,
		ZL_EXP_INT cur_line,
		char * str,
		int * start)
{
	......................................................
}

/**
 * T命令:获取栈追踪信息,以显示代码执行情况
 * 例如:
 * zl debug >>> T
 * /home/zengl/zenglBlog/admin/../mysql.zl:17 Mysql:init
 * /home/zengl/zenglBlog/admin/login.zl:24
 * zl debug >>>
 * 上面通过T命令,可以看到,当前执行到了mysql.zl脚本的第17行,并且是通过login.zl的第24行调用Mysql类的init方法进入到mysql.zl脚本的
 */
static void debug_command_stack_backtrace(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info)
{
	......................................................
}

/**
 * r命令:执行到返回
 * 如果当前位于某个脚本函数中,那么r命令会在脚本函数的调用位置的下一条指令位置处设置断点,并继续执行
 * 因此,使用r命令后,会马上执行完当前脚本函数,并在脚本函数返回时再触发断点,从而可以快速跳过某个函数的具体执行过程
 * 如果当前并不位于脚本函数中,就等效于c命令,也就是继续执行,直到遇到断点或者脚本结束
 * 例如:
 * zl debug >>> T
 * /home/zengl/zenglBlog/admin/../mysql.zl:17 Mysql:init
 * /home/zengl/zenglBlog/admin/login.zl:24
 * zl debug >>> r
 * file:/home/zengl/zenglBlog/admin/login.zl,line:25,breakIndex:0
 * zl debug >>> T
 * /home/zengl/zenglBlog/admin/login.zl:25
 * zl debug >>>
 * 上面当脚本位于Mysql类的init函数中时,通过r命令就可以一路执行完init函数,并在函数返回后的下一条指令位置处,也就是login.zl的第25行中断下来
 */
static void debug_command_run_to_return(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, int * exit)
{
	......................................................
}

/**
 * d命令:删除某个断点
 * 每个断点都有一个索引,可以通过B命令查看到,需要删除某个断点时,只要在d命令后面使用该断点的索引作为参数即可
 * 例如:
 * zl debug >>> B
 * [0] /home/zengl/zenglBlog/admin/login.zl:25 N:1 D:enable [current]
 * [1] /home/zengl/zenglBlog/admin/../mysql.zl:17 N:0 D:enable
 * total:2
 * zl debug >>> d 1
 * 删除断点成功
 * zl debug >>> B
 * [0] /home/zengl/zenglBlog/admin/login.zl:25 N:1 D:enable [current]
 * total:1
 * zl debug >>>
 * 上面通过d 1命令将索引为1的断点给删除掉
 */
static void debug_command_delete_breakpoint(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, char * str, int * start)
{
	......................................................
}

/**
 * D命令:禁用某个断点
 * 通过在D命令后面使用断点索引作为参数,就可以禁用某个断点
 * 例如:
 * zl debug >>> B
 * [0] /home/zengl/zenglBlog/admin/login.zl:25 N:1 D:enable [current]
 * [1] /home/zengl/zenglBlog/admin/login.zl:28 N:0 D:enable
 * total:2
 * zl debug >>> D 1
 * D命令禁用断点成功
 * zl debug >>> B
 * [0] /home/zengl/zenglBlog/admin/login.zl:25 N:1 D:enable [current]
 * [1] /home/zengl/zenglBlog/admin/login.zl:28 N:0 D:disable
 * total:2
 * zl debug >>>
 * 上面示例中,通过D 1命令将索引为1的断点给禁用掉,命令执行后,通过B命令可以看到该断点已经disable被禁用了
 * 断点被禁用后,脚本执行到断点位置处时,就不会被中断下来
 */
static void debug_command_disable_breakpoint(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, char * str, int * start)
{
	......................................................
}

/**
 * C命令:设置条件断点
 * 通过在C命令后面跟随断点索引和条件表达式,就可以在执行到断点位置处时,当条件表达式的结果不为0时中断下来
 * 例如:
 * zl debug >>> b 19
 * 设置断点成功
 * zl debug >>> B
 * [0] my_webroot/v0_8_0/test.zl:1 N:1 D:enable [current]
 * [1] my_webroot/v0_8_0/test.zl:19 N:0 D:enable
 * total:2
 * zl debug >>> C 1 json!=''
 * C命令设置条件断点成功
 * zl debug >>> B
 * [0] my_webroot/v0_8_0/test.zl:1 N:1 D:enable [current]
 * [1] my_webroot/v0_8_0/test.zl:19 C:json!=''; N:0 D:enable
 * total:2
 * zl debug >>> c
 * file:my_webroot/v0_8_0/test.zl,line:19,breakIndex:1
 * zl debug >>>
 * 上面通过 C 1 json!='' 命令在索引为1的断点处设置了条件表达式,当执行到test.zl的第19行时,如果json不等于空字符串,就中断下来
 * 通过B命令,也可以看到设置的条件表达式
 */
static void debug_command_set_condition_breakpoint(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info,
					char * str, int * start, int str_count, int str_size)
{
	......................................................
}

/**
 * L命令:设置日志断点
 * 通过在L命令后面跟随断点索引和日志表达式,可以将某断点转为日志断点,当执行到断点位置处时,会执行日志表达式,并将表达式的执行结果显示出来
 * 例如:
 * listen connection...
 * 127.0.0.1 connected:
 * file:my_webroot/v0_8_0/test.zl,line:1,breakIndex:0
 * zl debug >>> b 19
 * 设置断点成功
 * zl debug >>> L 1 json
 * L命令设置日志断点成功
 * zl debug >>> B
 * [0] my_webroot/v0_8_0/test.zl:1 N:1 D:enable [current]
 * [1] my_webroot/v0_8_0/test.zl:19 L:json; N:0 D:enable
 * total:2
 * zl debug >>> c
 * json :string:{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}
 * listen connection...
 * 上面通过L 1 json命令将索引为1的断点设置为了日志断点,通过B命令可以看到相关的日志表达式L:json;
 * 当执行到该断点位置处时,就会将表达式json;对应的值给显示出来
 */
static void debug_command_set_log_breakpoint(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info,
					char * str, int * start, int str_count, int str_size)
{
	......................................................
}

/**
 * N命令:设置断点次数
 * 通过在N命令后面跟随断点索引和断点次数参数,就可以为某断点设置允许中断的次数,当中断次数达到允许的值时,就会删除掉该断点
 * 例如:
 * zl debug >>> N 1 2
 * N命令设置断点次数成功
 * zl debug >>> B
 * [0] my_webroot/v0_8_0/test.zl:1 N:1 D:enable [current]
 * [1] my_webroot/v0_8_0/test.zl:30 N:2 D:enable
 * total:2
 * zl debug >>> c
 * file:my_webroot/v0_8_0/test.zl,line:30,breakIndex:1
 * zl debug >>> c
 * file:my_webroot/v0_8_0/test.zl,line:30,breakIndex:1
 * zl debug >>> c
 * listen connection...
 * 上面通过N 1 2命令将索引为1的断点设置了断点次数为2后,该断点就只会最多中断两次
 */
static void debug_command_set_breakpoint_number(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info, char * str, int * start)
{
	......................................................
}

/**
 * h命令:显示帮助信息,帮助信息中可以看到各个命令的基本用法
 */
static void debug_command_help(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info)
{
	builtin_make_info_string(VM_ARG, &debug_info->format_send_msg,
			" p 调试变量信息 usage:p express\n"
			" b 设置断点 usage:b filename lineNumber[ count] | b lineNumber[ count]\n"
			" B 查看断点列表 usage:B\n"
			" T 查看脚本函数的堆栈调用信息 usage:T\n"
			" d 删除某断点 usage:d breakIndex\n"
			" D 禁用某断点 usage:D breakIndex\n"
			" C 设置条件断点 usage:C breakIndex condition-express\n"
			" L 设置日志断点 usage:L breakIndex log-express\n"
			" N 设置断点次数 usage:N breakIndex count\n"
			" s 单步步入 usage:s\n"
			" S 单步步过 usage:S\n"
			" r 执行到返回 usage:r\n"
			" c 继续执行 usage:c\n"
			" l 显示源码 usage:l filename [lineNumber[ offset]] | l [lineNumber[ offset]]\n"
			" u 执行到指定的行 usage:u filename lineNumber | u lineNumber\n"
			" h 显示帮助信息\n");
}

/**
 * l命令:查看源码
 * 例如:
 * zl debug >>> l
 * current run line:1 [my_webroot/v0_8_0/test.zl]
 * 1    use builtin;    <<<---[ current line] ***
 * 2
 * 3    def TRUE 1;
 * ............................
 *
 * zl debug >>> l 8 10
 * current run line:1 [my_webroot/v0_8_0/test.zl]
 * 1    use builtin;    <<<---[ current line] ***
 * 2
 * ............................
 * 16
 * 17    json = '{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}';
 * 18
 *
 * zl debug >>> l test2.zl
 * 1    use builtin, request;
 * 2
 * 3    rqtSetResponseHeader("HTTP/1.1 302 Moved Temporarily");
 * 4    rqtSetResponseHeader("Location: test.zl");
 * 5    bltExit();
 *
 * zl debug >>> l test2.zl 5
 * ............................
 * zl debug >>> l test2.zl 5 10
 * ............................
 * 当l命令后面没有参数时,会将当前执行代码所在行附近的源码显示出来
 * 如果l命令后面只跟随了数字参数的话,那么分别表示需要显示的行号和偏移值
 * 例如上面的l 8 10表示将当前执行脚本的第8行附近的源码显示出来,10表示将第8行上下偏移10行的源码显示出来,因此会将1到18行的源码列举出来
 * 如果l命令后面跟随了脚本文件名的话,会将该脚本文件的源码显示出来,同样可以在脚本文件名后面跟随行号和偏移值
 * 和b命令类似,脚本文件名必须是相对于主执行脚本的相对路径
 * 假设主执行脚本是test.zl,并在test.zl中通过inc '../test2.zl'加载了test2.zl
 * 那么,要列举出test2.zl中的源码的话,就需要使用l ../test2.zl这样的写法
 *
 * zenglServer只会将脚本文件中的所有源码一次发给远程调试器,由远程调试器缓存源码,并根据行号等进行显示
 * 例如当在远程调试器中输入l test2.zl 5命令后,远程调试器只会将l test2.zl发送给zenglServer,zenglServer就会将test2.zl的所有内容一次读取出来
 * 并发送给远程调试器,远程调试器会将返回的源码缓存起来,然后从缓存的源码中,将第5行附近的代码列举出来,当下一次输入l test2.zl命令时
 * 远程调试器就只需要读取缓存即可,不需要再发送命令给zenglServer
 */
static int debug_command_list_file_content(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info,
		char * str, int * start, MAIN_DATA * my_data)
{
	......................................................
}

/**
 * u命令:执行到指定的行
 * u命令内部其实是先使用b命令在指定位置设置断点,并设置断点次数为1,然后继续执行,这样就可以快速的执行到指定位置,并在该位置中断下来
 * 例如:
 * zl debug >>> l 9 10
 * current run line:1 [my_webroot/v0_8_0/test.zl]
 * 1    use builtin;    <<<---[ current line] ***
 * 2
 * ........................................
 * 16
 * 17    json = '{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}';
 * 18
 * 19    json = bltJsonDecode(json);
 *
 * zl debug >>> u 19
 * file:my_webroot/v0_8_0/test.zl,line:19,breakIndex:1
 * zl debug >>> p json
 * json :string:{"hello": "world!!", "name": "zengl", "val": "programmer", "arr":[1,2,3]}
 * zl debug >>>
 * 上面一开始当前执行代码位于第一行,通过u 19命令可以直接执行到第19行,并在19行中断下来
 */
static int debug_command_until(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info,
		char * str, int * start, char * cur_filename, MAIN_DATA * my_data)
{
	int count = 1; // 设置断点次数为1,也就是只中断一次
	return debug_command_set_breakpoint(VM_ARG, debug_info, str, start, cur_filename, &count, my_data);
}

/**
 * 如果输入了无效的命令,则直接返回字符串“无效的命令”给远程调试器
 */
static void debug_command_invalid(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info)
{
	builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "无效的命令\n");
}

/**
 * 如果创建过调试相关的套接字,就通过close将其关闭掉
 */
static void debug_free_socket(int * debug_arg_socket)
{
	int debug_socket = (*debug_arg_socket);
	if(debug_socket != -1) {
		shutdown(debug_socket, SHUT_RDWR);
		if(close(debug_socket) == -1) {
			write_to_server_log_pipe(WRITE_TO_PIPE, "zl debug warning: close socket [%d] failed [%d] %s\n",
							debug_socket, errno, strerror(errno));
		}
		write_to_server_log_pipe(WRITE_TO_PIPE, "zl debug info: close socket [%d]\n", debug_socket);
		(*debug_arg_socket) = -1;
	}
}

/**
 * 通过调试相关的套接字,接收从远程调试器发来的信息,例如用户在远程调试器中输入的调试命令等
 * 接收到的数据会存储到server_reply对应的缓存中
 */
static int debug_recv(int sock, char * server_reply)
{
	int recv_num;
	if((recv_num = recv(sock , server_reply , DEBUG_RECV_SIZE , 0)) < 0) {
		write_to_server_log_pipe(WRITE_TO_PIPE, "zl debug error: recv failed. [%d] %s\n", errno, strerror(errno));
		return -1;
	}
	// 如果在等待接收的过程中,远程调试器终止连接,那么接收到的数据会为空,此时,recv会返回0
	if(recv_num == 0) {
		write_to_server_log_pipe(WRITE_TO_PIPE, "zl debug warning: recv 0 byte. maybe remote connection is closed\n");
		return -1;
	}
	return recv_num;
}

/**
 * 初始化DEBUG_INFO即调试相关的结构体,该结构体中存储了调试相关的套接字,以及需要发送给远程调试器的动态字符串
 */
void debug_init(DEBUG_INFO * debug_info)
{
	memset(debug_info, 0, sizeof(DEBUG_INFO));
	debug_info->socket = -1;
}

/**
 * 如果zenglServer开启了调试功能,那么,在zengl虚拟机关闭之前,需要调用此函数来关闭掉打开的调试套接字,以及释放掉分配过的动态字符串资源
 */
void debug_exit(ZL_EXP_VOID * VM_ARG, DEBUG_INFO * debug_info)
{
	if(debug_info->socket != -1) {
		// 关闭调试相关的套接字
		debug_free_socket(&debug_info->socket);
	}
	// 如果分配过动态字符串,则释放掉动态字符串
	if(debug_info->format_send_msg.str != NULL) {
		zenglApi_FreeMem(VM_ARG, debug_info->format_send_msg.str);
	}
}

/**
 * 中断回调函数,如果zenglServer开启了调试功能,那么当触发断点时,就会调用此回调函数
 * 在该回调函数中,可以接收远程调试器发来的各种调试命令,并将调试结果通过连接套接字反馈给远程调试器
 */
ZL_EXP_INT debug_break(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * cur_filename,
		ZL_EXP_INT cur_line,ZL_EXP_INT breakIndex,ZL_EXP_CHAR * log)
{
	MAIN_DATA * my_data = zenglApi_GetExtraData(VM_ARG, "my_data");
	DEBUG_INFO * debug_info = my_data->debug_info;
	int sock = debug_get_socket(VM_ARG, debug_info);
	int recv_num = 0;
	char server_reply[DEBUG_RECV_SIZE];
	if(sock == -1)
		return -1;

	builtin_reset_info_string(VM_ARG, &debug_info->format_send_msg);
	// 如果当前触发的是日志断点,那么就执行log日志表达式,并将表达式的执行结果反馈给远程调试器,如果执行成功,在反馈完结果后,会直接返回以继续执行
	// 因此,日志断点只会反馈表达式的结果,而不会停下来去接受调试命令,除非表达式执行失败,才会停下来接受命令,因为执行失败很可能是因为日志表达式存在语法错误
	// 停下来接受命令,可以重新设置正确的日志表达式
	if(log != ZL_EXP_NULL)
	{
		if(zenglApi_Debug(VM_ARG,log) == -1) // 如果日志表达式执行出错,则将错误信息反馈给调试器
		{
			write_to_server_log_pipe(WRITE_TO_PIPE, "log日志断点错误:%s",zenglApi_GetErrorString(VM_ARG));
			builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "log日志断点错误:%s",zenglApi_GetErrorString(VM_ARG));
			if(debug_socket_send(debug_info->socket, debug_info->format_send_msg.str, debug_info->format_send_msg.count) < 0)
				return -1;
			// 远程调试器在接收到数据后,会反馈一个字符串(例如反馈字符串"ok"回来)表示接收到了数据
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
			builtin_reset_info_string(VM_ARG, &debug_info->format_send_msg);
		}
		else
		{
			// 将调试结果转为字符串,存储到format_send_msg动态字符串中,并将其发送给远程调试器
			debug_make_value_str(VM_ARG, debug_info, log);
			if(debug_socket_send(debug_info->socket, debug_info->format_send_msg.str, debug_info->format_send_msg.count) < 0)
				return -1;
			// 远程调试器在接收到数据后,会反馈一个字符串(例如反馈字符串"ok"回来)表示接收到了数据
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
			return 0;
		}
	}
	// 如果是非日志断点,就将当前断点所在的脚本文件名,行号,断点索引等信息反馈给远程调试器
	builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"action\":\"debug\", "
					"\"filename\":\"%s\", \"line\":%d, \"breakIndex\":%d, \"main_script_filename\":\"%s\"}",
					cur_filename, cur_line, breakIndex, my_data->full_path);
	if(debug_socket_send(debug_info->socket, debug_info->format_send_msg.str, debug_info->format_send_msg.count) < 0)
		return -1;
	recv_num = 0;
	int exit = 0;
	const char * message = "";
	while(!exit)
	{
		// 接受用户输入的调试命令,如果没输入命令,会一直阻塞在这里,除非接收时发生错误,或者远程调试器关闭了连接
		recv_num = debug_recv(debug_info->socket, server_reply);
		if(recv_num < 0)
			return -1;
		server_reply[recv_num] = '\0';
		// 如果调试命令过长,则反馈警告信息
		if(recv_num >= DEBUG_RECV_SIZE - 10) {
			message = "zl debug warning: debugger command is too long\n";
			if(debug_socket_send(sock , (char *)message , strlen(message)) < 0)
				return -1;
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
			continue;
		}
		// 将接收到的调试命令记录到日志中
		write_to_server_log_pipe(WRITE_TO_PIPE, "zl debug info: debugger command: %s\n", server_reply);
		int start = 0;
		char * command;
		command = debug_get_arg(server_reply,&start,ZL_EXP_TRUE);
		if(command == ZL_EXP_NULL || strlen(command) != 1)
		{
			message = "命令必须是一个字符\n";
			if(debug_socket_send(sock , (char *)message , strlen(message)) < 0)
				return -1;
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
			continue;
		}
		builtin_reset_info_string(VM_ARG, &debug_info->format_send_msg);
		switch(command[0])
		{
		case 'p': // 执行表达式,并将表达式的结果反馈给远程调试器
			debug_command_print(VM_ARG, debug_info, server_reply, &start, recv_num, DEBUG_RECV_SIZE);
			break;
		case 'b': // 设置断点
			debug_command_set_breakpoint(VM_ARG, debug_info, server_reply, &start, cur_filename, NULL, my_data);
			break;
		case 'B': // 将设置过的断点都列举出来
			debug_command_list_breakpoints(VM_ARG, debug_info, breakIndex, cur_filename, cur_line, server_reply, &start);
			break;
		case 'T': // 获取栈追踪信息,以显示代码的执行情况
			debug_command_stack_backtrace(VM_ARG, debug_info);
			break;
		case 'r': // 执行到返回
			debug_command_run_to_return(VM_ARG, debug_info, &exit);
			builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"exit\":%d}", exit);
			break;
		case 'd': // 删除某个断点
			debug_command_delete_breakpoint(VM_ARG, debug_info, server_reply, &start);
			break;
		case 'D': // 禁用某个断点
			debug_command_disable_breakpoint(VM_ARG, debug_info, server_reply, &start);
			break;
		case 'C': // 设置条件断点
			debug_command_set_condition_breakpoint(VM_ARG, debug_info, server_reply, &start, recv_num, DEBUG_RECV_SIZE);
			break;
		case 'L': // 设置日志断点
			debug_command_set_log_breakpoint(VM_ARG, debug_info, server_reply, &start, recv_num, DEBUG_RECV_SIZE);
			break;
		case 'N': // 设置断点次数
			debug_command_set_breakpoint_number(VM_ARG, debug_info, server_reply, &start);
			break;
		case 's': // 单步步入,如果遇到脚本函数,则进入脚本函数
			zenglApi_DebugSetSingleBreak(VM_ARG,ZL_EXP_TRUE);
			exit = 1;
			builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"exit\":%d}", exit);
			break;
		case 'S': // 单步步过,如果遇到脚本函数,则直接执行完脚本函数,而不会进入脚本函数
			zenglApi_DebugSetSingleBreak(VM_ARG,ZL_EXP_FALSE);
			exit = 1;
			builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"exit\":%d}", exit);
			break;
		case 'c': // 继续执行
			exit = 1;
			builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"exit\":%d}", exit);
			break;
		case 'h': // 显示帮助信息
			debug_command_help(VM_ARG, debug_info);
			break;
		case 'l': // 查看源码
			if(debug_command_list_file_content(VM_ARG, debug_info, server_reply, &start, my_data) < 0) {
				return -1;
			}
			builtin_reset_info_string(VM_ARG, &debug_info->format_send_msg);
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
			break;
		case 'u': // 执行到指定的行
			exit = 1;
			if(debug_command_until(VM_ARG, debug_info, server_reply, &start, cur_filename, my_data) == 0) {
				builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "{\"exit\":%d}", exit);
			}
			else
				exit = 0;
			break;
		default: // 无效的命令
			debug_command_invalid(VM_ARG, debug_info);
			break;
		}
		if(debug_info->format_send_msg.count > 0) { // 如果在命令执行过程中设置了动态字符串的话,就将动态字符串发送给远程调试器
			if(debug_socket_send(debug_info->socket, debug_info->format_send_msg.str, debug_info->format_send_msg.count) < 0)
				return -1;
			// 远程调试器在接收到数据后,会反馈一个字符串(例如反馈字符串"ok"回来)表示接收到了数据
			recv_num = debug_recv(debug_info->socket, server_reply);
			if(recv_num < 0)
				return -1;
		}
	}
	return 0;
}

/**
 * 在设置条件断点时,如果设置的条件表达式有错误(例如语法错误等),那么当条件表达式执行出错时,就会触发下面的回调函数
 * 在该回调函数中,会将出错信息反馈给远程调试器
 */
ZL_EXP_INT debug_conditionError(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * filename,
				ZL_EXP_INT line,ZL_EXP_INT breakIndex,ZL_EXP_CHAR * error)
{
	MAIN_DATA * my_data = zenglApi_GetExtraData(VM_ARG, "my_data");
	DEBUG_INFO * debug_info = my_data->debug_info;
	int sock = debug_get_socket(VM_ARG, debug_info);
	if(sock == -1)
		return -1;
	char * condition;
	zenglApi_DebugGetBreak(VM_ARG,breakIndex,ZL_EXP_NULL,ZL_EXP_NULL,&condition,ZL_EXP_NULL,ZL_EXP_NULL,ZL_EXP_NULL,ZL_EXP_NULL);
	write_to_server_log_pipe(WRITE_TO_PIPE, "\nzl debug condition error:%s [%d] <%d %s> error:%s\n",filename,line,breakIndex,condition,error);
	builtin_reset_info_string(VM_ARG, &debug_info->format_send_msg);
	builtin_make_info_string(VM_ARG, &debug_info->format_send_msg, "\nzl debug condition error:%s [%d] <%d %s> error:%s\n",
					filename,line,breakIndex,condition,error);
	if(debug_socket_send(debug_info->socket, debug_info->format_send_msg.str, debug_info->format_send_msg.count) < 0) {
		return -1;
	}
	char server_reply[DEBUG_RECV_SIZE];
	int recv_num = debug_recv(debug_info->socket, server_reply);
	if(recv_num < 0)
		return -1;
	return 0;
}


    限于篇幅,本章就到这里,读者可以通过阅读源码和相关注释来加深理解。

结束语:

    控制愤怒最好的方法就是控制你的身体

——  《绿巨人2》
 
上下篇

下一篇: zenglServer v0.10.0 使用zengl脚本的编译缓存,跳过编译过程

上一篇: zenglServer v0.8.0-v0.8.1

相关文章

zenglServer v0.15.0 - v0.15.1 增加curl模块,用于执行数据抓取操作

zenglServer v0.5.0 设置响应头, 获取cookie

zenglServer v0.16.0 增加curlSetPostByHashArray,curlSetHeaderByArray模块函数,调整进程名称等

zenglServer v0.7.0-v0.7.1 mustache模板解析

zenglServer v0.11.0 共享内存,crustache转义,magick模块,新增bltDate等内建模块函数

zenglServer v0.10.1 添加bltInt,bltFloat,bltHtmlEscape模块函数,使用v1.8.1版本的zengl语言库