之前利用acra的开源框架实现过一个app日志自动收集功能,数据库用couchDB,app产生错误事件时自动上传日志,一开始还挺好用的,后来数据量越来越大,服务器不堪重负,导致经常性的发生服务器卡死,索性又开发了一个用户点击上传日志的功能,只是存在另一个服务器上。(后来才知道给我们分配的服务器内存只有2G,好坑)。
先要在服务器上搭建FTP服务,linux系统配置vsfptd的文章到处都是,要在配置文件里配置下,然后编辑chroot_list和/etc/vsftpd/vsftpd.user_list,添加用户列表,创建一个文件夹用于存放文件,启动ftp服务后就可以用filezilla试下能不能连上服务器,需要注意的是vsftpd默认端口是21,filezilla连的时候不需要配端口,如果能进入之前创建的文件夹说明配置好了。
之后去下载apache commons-net-3.x.x的jar包,里面包含ftp客户端所需功能,然后就可以开始写代码了。
一开始觉得这功能挺单纯的,就是一个button的点击事件,然后收集Logcat,调用ftp的api上传,但要考虑过程中存在的异常事件并把代码写得比较完善还是花了较多时间,最后实现的流程如下:
解释下,彩蛋是一个隐藏的弹窗,类似手机的开发者模式,多次点击后会弹出这样一个窗口,里面包含一些研发人员关注的信息,日志上传的按钮就在这个窗口里。既然是点击事件,从上往下梳理下写代码的思路
一开始没有想清楚,直接从抓log,ftp上传开始写代码,功能是实现了,发现不实用,首先点击Button后如果长时间没有响应会报ANR,就是说点击后必须立即开启新线程;然后是最后的结果如果通知到上传界面,不光是ftp上传,logcat的抓取,文件的读写操作都需要加try-catch,任何一个流程出错都需要通知到UI,想了一下觉得用AsyncTask配合Interface实现。
public interface IAsyncResponse {
void onTaskSuccess(String[] files);
void onTaskFailed(int ret);
}
成功进入onTaskSuccess,传入文件名,在界面上提示用户上传的文件;失败进入onTaskFailed,提示错误码。AsyntTask的结构就是下面这样:
public class UploatLogcatTask extends AsyncTask<Context, Void, Integer> {
private Context context;
private IAsyncResponse asyncResponse;
public UploatLogcatTask(Context ctx, IAsyncResponse res){
this.context = ctx;
asyncResponse = res;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Integer doInBackground(Context... params) {
.........
//任何一个流程出错直接返回错误码
return ret;
}
@Override
protected void onPostExecute(Integer integer) {
if (integer == FTP_UPLOAD_SUCC){
asyncResponse.onTaskSuccess(logFiles);
}else {
asyncResponse.onTaskFailed(integer);
}
}
}
IAsyncResponse在构造函数中传入,在UI调用的时候就是这样:
iAsyncResponse = new IAsyncResponse() {
@Override
public void onTaskSuccess(String[] files) {
}
@Override
public void onTaskFailed(int ret) {
}
};
uploadLogcatBT.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UploatLogcatTask uploatLogcatTask = new UploatLogcatTask(getApplication(), iAsyncResponse);
uploatLogcatTask.execute(getApplicationContext());
}
});
收集logcat、写入文件、上传文件任何一步出错都能返回到UI,出错之后不再执行下面的流程。
理清整个流程后,下面就是分别实现各个功能。
收集logcat,这是把Logcat存为一个string,其实可以直接保存为文件,但不知道是不是参数没有设对,一直没成功,不得已这么做:
static String collectLogCatToString() {
ArrayList commandLine = new ArrayList();
String[] defaultValues = new String[]{"logcat", "-d", "threadtime"};
ArrayList logcatArgumentsList = new ArrayList(Arrays.asList(defaultValues));
commandLine.addAll(logcatArgumentsList);
Log.i(CLASS_TAG, "commandLine: " + commandLine);
String logcatBuf = "";
try {
final java.lang.Process e = Runtime.getRuntime().exec((String[])commandLine.toArray(new String[commandLine.size()]));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(e.getInputStream()), 8192);
while(true) {
String line;
line = bufferedReader.readLine();
logcatBuf = logcatBuf + line + "\n";
if(line == null) {
return logcatBuf;
}
}
} catch (IOException var11) {
return null;
}
}
给文件命名:
/**
* @return 后缀.log
*/
static String setLogFileName(){
String phoneNumber = "";
String tfNumber = "";
String time = "";
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
time = sdf.format(new Date());
phoneNumber = "13421211214";
tfNumber = "1111122222";
fileName = phoneNumber +"_" + tfNumber +"_" + time + ".log";
return fileName;
}
/**
* 保存抓到的Logcat到文件
*/
static int writeStringToFile(Context context,String message,String fileName) {
Log.i(CLASS_TAG,"writeStringToFile called...");
String locatLogPath = context.getApplicationContext().getFilesDir().getAbsolutePath();
final String command = "chmod 777 " + locatLogPath;
try {
Runtime.getRuntime().exec(command);
} catch (IOException e) {
e.printStackTrace();
return CHMOD_ERR;
}
FileOutputStream outStream;
try {
outStream = context.openFileOutput(fileName,
MODE_PRIVATE);
} catch (FileNotFoundException e) {
e.printStackTrace();
return FILE_NOT_FOUND;
}
// 将文本转换为字节集
byte[] data = message.getBytes();
try {
// 写出文件
outStream.write(data);
outStream.flush();
outStream.close();
Log.i(CLASS_TAG,"writeStringToFile end...");
} catch (IOException e) {
e.printStackTrace();
return WRITE_FILE_ERR;
}
return CREAT_LOGFILE_SUCC;
}
追加文件内容:
/**
* 追加文件:使用FileWriter
*
* @param fileName
* @param content
*/
static void appendStringToFile(Context context, String fileName, String content) {
String locatLogPath = context.getApplicationContext().getFilesDir().getAbsolutePath();
fileName = locatLogPath + File.separator + fileName;
FileWriter writer = null;
try {
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
writer = new FileWriter(fileName, true);
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(writer != null){
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件重命名:
static int renameFileName(Context context, String oldFile, String newFile){
if (oldFile == null || newFile == null){
Log.e(CLASS_TAG, "renameFileName err...");
}
String locatLogPath = context.getApplicationContext().getFilesDir().getAbsolutePath();
String oldFilePath = locatLogPath + File.separator + oldFile;
String newFilePath = locatLogPath + File.separator + newFile;
File file1 = new File(oldFilePath);
File file2 = new File(newFilePath);
if (file1.renameTo(file2)){
return RENAME_FILE_SUSS;
}else {
return RENAME_FILE_FAILED;
}
}
查找是否存在旧日志文件:
/**
* 查找.log文件
* @param context
* @return
*/
static String[] searchOldLogFiles(Context context){
File f = new File(context.getApplicationContext().getFilesDir().getAbsolutePath());
MyFilter myFilter = new MyFilter(".log", "ecmlog.log");
return f.list(myFilter);
}
private static class MyFilter implements FilenameFilter{
String type;
String exclude;
MyFilter(String a, String ex){
type = a;
exclude = ex;
}
@Override
public boolean accept(File dir, String filename) {
return filename.endsWith(type) && !filename.equals(exclude);
}
}
ftp上传:
/**
* 通过ftp上传文件
* @param url ftp服务器地址 如: 192.168.2.111
* @param port 端口如 : 21
* @param username 登录名
* @param password 密码
* @param remotePath 上到ftp服务器的磁盘路径
* @param fileNamePath 要上传的文件路径
* @param fileName 要上传的文件名
* @return
*/
private static int ftpUpload(String url, String port, String username,String password, String remotePath, String fileNamePath,String fileName) {
FTPClient ftpClient = new FTPClient();
FileInputStream fis = null;
int result = 0;
try {
ftpClient.setConnectTimeout(5 * 1000);
ftpClient.connect(url, parseInt(port));
boolean loginResult = ftpClient.login(username, password);
int returnCode = ftpClient.getReplyCode();
if (loginResult && FTPReply.isPositiveCompletion(returnCode)) {// 如果登录成功
ftpClient.makeDirectory(remotePath);
// 设置上传目录
ftpClient.changeWorkingDirectory(remotePath);
ftpClient.setBufferSize(1024);
ftpClient.setControlEncoding("UTF-8");
ftpClient.enterLocalPassiveMode();
fis = new FileInputStream(fileNamePath + "/" + fileName);
ftpClient.storeFile(fileName, fis);
result = FTP_UPLOAD_SUCC; //上传成功
} else {// 如果登录失败
result = FTP_LOGIN_ERR;
}
} catch (IOException e) {
e.printStackTrace();
result = FTP_CLIENT_ERR;
} finally {
try {
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
remotePath是服务器上的文件夹路径,不用包含完整路劲,比如“/ftpusername/logcat”。
下面附上完整调用代码:
public class UploatLogcatTask extends AsyncTask<Context, Void, Integer> {
private final String CLASS_TAG = "UploatLogcatTask";
private Context context;
private IAsyncResponse asyncResponse;
/**
* .log文件
*/
private String[] logFiles;
public UploatLogcatTask(Context ctx, IAsyncResponse res){
this.context = ctx;
asyncResponse = res;
}
@Override
protected void onPreExecute() {
//查找是否有上传失败的日志
logFiles = LogcatUploaderUtils.searchOldLogFiles(context);
super.onPreExecute();
}
@Override
protected Integer doInBackground(Context... params) {
int ret = FTP_UPLOAD_SUCC;
String log = collectLogCatToString();
String logFileName = setLogFileName();
//上传新日志
if (logFiles == null || logFiles.length == 0){
logFiles = new String[1];
writeStringToFile(context, log, logFileName);
ret = uploadLogcat(context, logFileName);
if (ret == FTP_UPLOAD_SUCC){
deleteOldLogcatFile(context, logFileName);
logFiles[0] = logFileName;
}
}else {
//旧日志追加新日志上传
int index = 0;
for (String file: logFiles){
appendStringToFile(context,file, log);
ret = renameFileName(context,file, logFileName);
if (ret == RENAME_FILE_FAILED){
return ret;
}
ret = uploadLogcat(context, logFileName);
if (ret != FTP_UPLOAD_SUCC){
return ret;
}
deleteOldLogcatFile(context, logFileName);
logFiles[index++] = logFileName;
}
}
return ret;
}
@Override
protected void onPostExecute(Integer integer) {
if (integer == FTP_UPLOAD_SUCC){
asyncResponse.onTaskSuccess(logFiles);
}else {
asyncResponse.onTaskFailed(integer);
}
}
}
有些函数也很简单,看名字就看得出来,就不贴了。
记得加入权限:
<uses-permission android:name="android.permission.READ_LOGS"/>
这种写法可以把各个部分功能尽量的简化,并且没有太多的耦合,UI调用也很简单,成功或失败返回码也明确,测试同事测了一轮说有个小问题就是正在上传日志的时候把网络变成假网络(就是把路由器和外网的连接断开,手机还是连上路由)会一直卡在上传中的状态,由于ftp部分功能我是直接用的jar包,这种情况下如果没有抛出exception也不知道怎么处理,也希望有人看到代码能给出一种可行的方法。因为是以弹窗activity的形式出现的,我的做法是设置当点击窗口外部区域时activity自动关闭,这样不会卡死。