发送异常堆栈到企业微信

What & Why

最近项目收尾有空闲时间,于是想完善一下工具链。接入了DebugDrawer,加了不少小功能功能,还蛮欣喜的。
后来想想流程上的一些细节,想到了测试在反馈崩溃bug的时候,我们经常要拿他们的测试机过来,连上手机以后,根据日志里的崩溃信息来定位问题。
这个步骤有点繁琐,于是决定写个小工具,debug版本测试的时候,如果遇到了崩溃,直接把异常堆栈发到我们的聊天软件(企业微信)可好?
看了下企业微信有提供发送消息的api,只要把异常信息通过内网服务器转发到企业微信就好了。

大致流程

  • app崩溃
  • 捕获异常
  • 重启应用
  • (弹窗提示是否发送崩溃堆栈到服务器)发送异常堆栈、账号、等信息到服务器
  • 服务器转发异常到企业微信

#服务器端转发代码见附录

遇到的问题

android这边,debug代码默认是不混淆的,所以从异常堆栈可以直接看出出错的代码位置,但是和ios那边沟通了一下,发现他们的异常堆栈需要用符号表解析一下才可用。
他们也有个类似mapping的文件叫dSYM(dSYM其实是个目录,实际的mapping文件为:dSYM/Contents/Resources/DWARF/${appName}),需要用xcrun atos把异常堆栈解析成可读的代码。
因为我们的编译机就是mac系统,所以自带xcrun,写个脚本转一下就好了。

How

解析ios异常堆栈的代码

analysisIOSCrash.sh

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
#!/bin/bash
#############################
#
# usage: ./analysisIOSCrash.sh crashFile dSYMKey appName arch outputAll [resultFile]
#
# crashFile: 保存异常堆栈的文件
# dSYMKey: dSYM目录,这里是用build号标识的,dSYM目录存储路径为:当前脚本路径/dSYM/$appName/$dSYMKey
# appName: app bundle名
# arch: 异常发生的机器cpu架构
# outputAll:
# 0: 只输出appName对应的转换后的异常信息
# 1: 输出转换后的整个异常栈
# resultFile: 将结果保存到$resultFile,如果没有这个参数则直接输出到控制台,可选参数
#############################
if [ $# -lt 5 ]; then
echo "param num not fit"
exit 1
fi
crashFile=$1
dSYMKey=$2
dSYM_DIR=`pwd`/dSYM
appName=$3
dSYMFile=$dSYM_DIR/$appName/$dSYMKey
arch=$4
outputAll=$5
resultFile=`pwd`/`date +%s`".tmp"
keepResult=0
if [ $# == 6 ]; then
resultFile=$6
keepResult=1
fi
if [ ! -d "$dSYMFile" ]; then
echo "not found dSYMFile: $dSYMFile"
exit 1
fi
if [ ! -f "$crashFile" ]; then
echo "not found crashFile: $crashFile"
exit 1
fi
if [ $outputAll == 1 ]; then
awk -v dSYM="$dSYMFile" -v appName=$appName -v arch=$arch '{if ($0 ~ /'$appName'[\t ]*0x[0123456789abcdefABCDEF]+[\t ]+'$appName'/) {value=int($3) - int($6); addr="0x"sprintf("%lx", value); dSYM2=dSYM"/Contents/Resources/DWARF/"appName; "xcrun atos -arch "arch" -o \""dSYM2"\" -l "addr" "$3"" | getline result; printf("\t%-4d%-35s %s %s\n", $1, $2, $3, result);} else {print $0;}}' "$crashFile" > $resultFile 2>&1
else
awk -v dSYM=$dSYMFile -v appName=$appName -v arch=$arch 'BEGIN{opLine=1;}{if($0 ~ /^\(/){print "("; opLine=0;} if($0 ~ /^\)/){opLine=1;} if ($0 ~ /'$appName'[\t ]*0x[0123456789abcdefABCDEF]+[\t ]+'$appName'/) {value=int($3) - int($6); addr="0x"sprintf("%lx", value); dSYM2=dSYM"/Contents/Resources/DWARF/"appName; "xcrun atos -arch "arch" -o \""dSYM2"\" -l "addr" "$3"" | getline result; printf("\t%-4d%-35s %s %s\n", $1, $2, $3, result);} else {if(opLine == 1) {print $0;}}}' $crashFile > $resultFile 2>&1
fi
cat $resultFile
if [ $keepResult == 0 ]; then
rm $resultFile
fi
exit 0

服务器端转发代码

使用的时候需要替换 cropidcropsecretagentid这几个参数

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
<?php
/****************************
*
* 参数:
* p: ios/android
* app: app名称
* msg: 额外消息
* stack: 异常堆栈
* archive: cpu架构(ios额外参数)
* dSYMId:dSYM文件名,这里用的是build号(ios额外参数)
*
****************************/
function getToken() {
$url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${这里填入cropId}&corpsecret=${这里填入应用的corpsecret}";
$result = json_decode(file_get_contents($url));
//TODO 如果高频率请求token,后面可能会收不到消息,这两个参数存数据库
//$result->access_token
//$result->expires_in
return $result->access_token;
}
function http_post_data($url, $data) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charset=utf-8',
'Content-Length: ' . strlen($data)
));
ob_start();
$return_content = curl_exec($ch);
curl_close($ch);
if(!empty($return_content)) {
try {
$result = json_decode($return_content);
$code = $result->errcode;
if($code == 0) {
return 'success';
} else {
return $result->errmsg;
}
} catch (Exception $e) {
return $e->getMessage();
}
} else {
return 'failed!!';
}
}
function sendMsg($totag, $msg) {
$url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" . getToken();
$target_msg = array(
'totag' => $totag,
'agentid' => '${这里填入公司的agentId}',
'msgtype' => 'text',
'text' => array(
'content' => $msg
)
);
$encode_msg = json_encode($target_msg);
return http_post_data($url, $encode_msg);
}
//检查参数
if(!(array_key_exists('p', $_POST)
&& array_key_exists('app', $_POST)
&& array_key_exists('msg', $_POST)
&& array_key_exists('stack', $_POST))) {
echo "need param!";
return ;
}
$platform = $_POST['p'];
$app = $_POST['app'];
$msg = $_POST['msg'];
$stack = $_POST['stack'];
$archive = '';
$dSYMId = '';
if(empty($platform) || empty($app) || empty($msg) || empty($stack)) {
echo "empty param!";
return ;
}
if($platform != 'android' && $platform != 'ios') {
echo "invalidate param app";
return ;
}
if($platform == 'ios') {
if(!array_key_exists('archive', $_POST) || !array_key_exists('dSYMId', $_POST)) {
echo 'need param!';
}
$archive = $_POST['archive'];
$dSYMId = $_POST['dSYMId'];
if(empty($archive) || empty($dSYMId)) {
echo "empty param!";
return ;
}
$crash_dir = "crash";
if(!is_dir($crash_dir)) {
mkdir($crash_dir, 0777);
}
//将错误堆栈写入临时文件
$crashFileName = $crash_dir . "/". time() . ".txt";
$resultFileName = $crash_dir . "/" . time() . "-result.txt";
$crashFile = fopen($crashFileName, "w");
fwrite($crashFile, $stack);
fclose($crashFile);
$dSYMFile = "" . $dSYMId . ".dSYM";
$showAll = strlen($msg . $stack) > 2000 ? 0 : 1;
$cmd = "./analysisIOSCrash.sh " . $crashFileName . " " . $dSYMFile . " " . $app . " " . $archive . " " . $showAll . " " . $resultFileName;
exec($cmd, $dSYM_result, $r_code);
if($r_code == 0) {
$dSYM_result = file_get_contents($resultFileName);
if(!empty($dSYM_result)) {
$stack = $dSYM_result;
}
} else {
echo "error: ";
var_dump($dSYM_result);
$stack = "\n!!!!!!!!!!!!!!! ERROR:\n " . $dSYM_result[0] . "\n!!!!!!!!!!!!!!!\n\n" . $stack;
}
unlink($crashFileName);
unlink($resultFileName);
}
$totag = $platform == 'android' ? '1' : '2';
$msg = "应用: " . $app . "\n" . $msg . "\n" . $stack;
echo sendMsg($totag, $msg);
?>

结语

程序员需要学会更多偷懒的姿势😁

热评文章