123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- //
- // VclCrash.m
- // VclCrash
- //
- // Created by Felix on 2018/8/10.
- // Copyright © 2018年 Felix. All rights reserved.
- //
- #import "VclCrash.h"
- #import "VclSystemInfo.h"
- #import <Foundation/NSURLSession.h>
- #import <UIKit/UIKit.h>
- #import "Domains.h"
- #import "BuryPointModel.h"
- #define LOG_TIME_FORMAT @"yyyy-MM-dd HH:mm:ss.SSS"
- #define VclCRASH_FORM_BOUNDARY @"VclCRASH_FORM_BOUNDARY"
- #define HTTP_REQUEST_URL_STRING @"http://dev2.ipc.Vcl.optjoy.io:30288/apis/v1/logfile/logfile/upload/app/ios/resource?uid=12345&access_token=9999"
- // 目前開發用的域名是 dev env的域名 請在正式環境改成 stag env的域名
- // stag env 下
- // 國內地區存儲在 qcloud的logfiles-stag bucket
- // 海外地區存儲在 gcp的logfiles-stag bucket
- // 正式版 國內:https://stag.ipc.Vcl.optjoy.cn:30011/apis/v1 海外:https://stag.ipc.Vcl.optjoy.io:30011/apis/v1
- #define HTTP_REQUEST_URL_STRING_CN @"https://stag.ipc.Vcl.optjoy.cn:30011/apis/v1/logfile/logfile/upload/app/ios/resource?uid=12345&access_token=9999"
- #define HTTP_REQUEST_URL_STRING_IO @"https://stag.ipc.Vcl.optjoy.io:30011/apis/v1/logfile/logfile/upload/app/ios/resource?uid=12345&access_token=9999"
- #define HTTP_REQUEST_URL_STRING_IO_DF @"https://ipc.dada.p.optjoy.io:30011/apis/v1/logfile/logfile/upload/app/ios/resource?uid=12345&access_token=9999"
- @interface VclCrash () <NSURLSessionDelegate>
- @property (nonatomic, copy) NSMutableArray<NSString *> *pendingUploads; // 待上传的文件所在路径;
- @property (nonatomic, assign) BOOL isFormDataEmpty; // 是否为空表单上传,默认YES
- @property (nonatomic, copy) NSString *currentReport; // 程序本次启动新创建的文件名,上方待上传的文件pendingUploads不包含新创建的文件
- @end
- static VclCrash *_instance = nil;
- @implementation VclCrash
- + (instancetype)sharedInstance {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- _instance = [[VclCrash alloc] init];
- });
- return _instance;
- }
- + (instancetype)allocWithZone:(struct _NSZone *)zone {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- _instance = [super allocWithZone:zone];
- });
- return _instance;
- }
- - (instancetype)copyWithZone:(NSZone *)zone {
- return _instance;
- }
- - (instancetype)init {
- return [self initWithBasePath:self.getBasePath];
- }
- - (instancetype)initWithBasePath:(NSString *)basePath {
- if (self = [super init]) {
-
- // 0.initialized data
- self.isFormDataEmpty = YES;
-
- // app启动的时候需要做的事情
- // 1.设置异常监听
- //[self setDefaultUncaughtExceptionHandler];
-
- // 2.获取已存在的异常文件的文件路径
- [self getPendingUploadFiles];
-
- // 3.创建本次的启动日志文件
- [self createNewReportFile];
-
- // // 4.上传已存在的日志文件
- // [self uploadReportFiles]; /*暂时不上传*/
-
- // 4.创建本次的崩溃文件日志文件
- [self createCrashReportFile];
-
- // 5.获取数据上传到服务器
- [self uploadCrashReportFiles];
- }
- return self;
- }
- // 程序崩溃时回调函数
- void UncaughtExceptionHandler(NSException *exception) {
- // 出现异常的堆栈信息
- NSArray *callStackSymbols = [exception callStackSymbols];
-
- // 出现异常的原因
- NSString *reason = [exception reason];
-
- // 异常的名称
- NSString *name = [exception name];
-
- NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
- [dateFormatter setDateFormat:LOG_TIME_FORMAT];
-
- // 出现异常的时间
- NSString *time = [dateFormatter stringFromDate:[NSDate date]];
-
- NSString *exceptionInfo = [NSString stringWithFormat:@"Exception Time:%@\nException Name:%@\nException Reason:%@\nException Stack:%@", time, name, reason, callStackSymbols];
- // [[VclCrash sharedInstance] writeCrashContent:exceptionInfo];
- // 以写文件的方式把奔溃信息写入文件,BuryPointModel
- [[VclCrash sharedInstance] writeCrashContent:[BuryPointModel crashModelWithString:exceptionInfo]];
- }
- // 拦截signal
- void SignalHandler(int signal) {
-
- }
- - (void)setDefaultUncaughtExceptionHandler {
- NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
- signal(SIGABRT, SignalHandler);
- signal(SIGILL, SignalHandler);
- signal(SIGSEGV, SignalHandler);
- signal(SIGFPE, SignalHandler);
- signal(SIGBUS, SignalHandler);
- signal(SIGPIPE, SignalHandler);
- }
- //createCrashReportFile
- // 将信息写入到
- - (void)writeContent:(NSString *)content {
- [self write:content filename:self.currentReport];
- }
- // 崩溃信息写入到崩溃日志
- - (void)writeCrashContent:(NSString *)content {
- [self write:content filename:[self createCrashReportFile]];
- }
- - (void)write:(NSString *)content filename:(NSString *)filename {
- if (!filename) {
- return;
- }
- @try {
- NSString *path = [self getReportsDirectory];
- if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
- // 创建日志文件的绝对路径
- [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
- }
-
- NSString *filePath = [path stringByAppendingPathComponent:filename];
- if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
- [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
- return;
- }
-
- NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
- // 将写入文件操作的指针跳到文件的末端
- [fileHandle seekToEndOfFile];
- // 追加写入数据
- [fileHandle writeData:[[NSString stringWithFormat:@"\n%@", content] dataUsingEncoding:NSUTF8StringEncoding]];
- [fileHandle closeFile];
- }
- @catch (NSException *exception) {
-
- }
- @finally {
-
- }
- }
- - (void)clearCrashFile {
- [self clearFile:self.currentReport];
- }
- - (void)clearFile:(NSString *)filename {
- @try {
- NSString *path = self.getReportsDirectory;
- if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
- // 创建日志文件的绝对路径
- [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
- }
-
- NSString *filePath = [path stringByAppendingPathComponent:filename];
- if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
- NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
- // 将写入文件操作的指针跳到文件的末端
- [fileHandle truncateFileAtOffset:0];
- [fileHandle closeFile];
- }
- }
- @catch (NSException *exception) {
- HLog(@"[VclCrash] clear local file error with exception:\n%@", exception);
- }
- @finally {
-
- }
- }
- /**
- 根据本地文件的全路径,删除文件
-
- @param fullPath 全路径
- */
- - (BOOL)removeLocalFileWithFullPath:(NSString *)fullPath {
- BOOL result = NO;
- NSError *error = nil;
- if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
- result = [[NSFileManager defaultManager] removeItemAtPath:fullPath error:&error];
- }
- if (error) {
- HLog(@"[VclCrash] remove local file with error: %@.", error);
- }
- return result;
- }
- - (void)uploadReportFiles {
- NSArray *paths = [self.pendingUploads copy];
- [self uploadWithFilePaths:paths params:@{@"userName" : @"999", @"uid" : @"12345", @"access_token": @"9999", @"type" : @"txt"}];
- }
- /**
- 上传多个文件
-
- @param filePaths 待上传的文件所在路径
- @param params 参数
- */
- - (void)uploadWithFilePaths:(NSArray<NSString *> *)filePaths params:(NSDictionary *)params {
- NSURL *url = [NSURL URLWithString:[self getHttpRequestURLString]];
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
-
- // 分界线 --VclCRASH_FORM_BOUNDARY
- NSString *BOUNDARY = [[NSString alloc] initWithFormat:@"\r\n--%@\r\n", VclCRASH_FORM_BOUNDARY];
-
- // 结束符:--VclCRASH_FORM_BOUNDARY--
- NSString *BOUNDARY_EOF = [[NSString alloc] initWithFormat:@"\r\n--%@--\r\n", VclCRASH_FORM_BOUNDARY];
-
- // 声明requestData,用来放入http body
- NSMutableData *requestData = [NSMutableData data];
-
- // 时间格式对象
- NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
-
- NSMutableArray *emptyFileFullPaths = [NSMutableArray array];
-
- // 参数的集合的所有key的集合
- NSString *uuid = [self fetchDeviceUUID];
- NSString *headerFlag = [NSString stringWithFormat:@"%@_", uuid];
- for (NSString *obj in filePaths) {
- NSData *data = [NSData dataWithContentsOfFile:obj];
- // 如果是有数据才进行拼接并且上传
- if (data.length) {
- // http body的字符串
- NSMutableString *body = [[NSMutableString alloc] init];
- [body appendFormat:@"%@", BOUNDARY];
- dateFormatter.dateFormat = @"yyyy.MM.dd_HH.mm.ss.sss";
- NSString *fileName = [obj lastPathComponent];
-
- fileName = [headerFlag stringByAppendingString:fileName];
- [body appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fileName, fileName];
- [body appendFormat:@"Content-Type: \"%@\";\r\n\r\n", @"text/plain"];
- // 将body字符串转化为UTF8格式的二进制
- [requestData appendData:[body dataUsingEncoding:NSUTF8StringEncoding]];
- [requestData appendData:[[VclSystemInfo sharedInstance] systemInfoData]];
- [requestData appendData:data];
-
- self.isFormDataEmpty = NO;
- }
- else { // 否则本地的空文件直接删除
- if ([self.pendingUploads containsObject:obj]) {
- [self.pendingUploads removeObject:obj];
- [emptyFileFullPaths addObject:obj];
- }
- }
- }
-
- for (NSString *path in emptyFileFullPaths) {
- [self removeLocalFileWithFullPath:path];
- }
-
- // 加入结束符
- [requestData appendData:[BOUNDARY_EOF dataUsingEncoding:NSUTF8StringEncoding]];
-
- // 如果上传的文件都为空,则不进行下方的上传操作
- if (self.isFormDataEmpty) {
- return;
- }
-
- // 设置HTTPHeader
- // 设置HTTPHeader中Content-Type的值
- NSString *content = [[NSString alloc] initWithFormat:@"multipart/form-data; boundary=%@", VclCRASH_FORM_BOUNDARY];
- [request setValue:content forHTTPHeaderField:@"Content-Type"];
-
- // 设置Content-Length
- [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)requestData.length] forHTTPHeaderField:@"Content-Length"];
-
- // 设置时间戳
- [dateFormatter setDateFormat:@"yyyyMMddHHmmssSSS"];
- NSString *timestamp = [dateFormatter stringFromDate:[NSDate date]];
- [request setValue:timestamp forHTTPHeaderField:@"Timestamp"];
-
- // 设置app/{{platform}}/下的子目录
- [dateFormatter setDateFormat:@"yyyyMMdd"];
- NSString *directory = [dateFormatter stringFromDate:[NSDate date]];
- [request setValue:directory forHTTPHeaderField:@"Filename"];
-
- // 设置http body
- [request setHTTPBody:requestData];
-
- // http method
- [request setHTTPMethod:@"POST"];
-
- // URLSession
- NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
- NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:Nil];
- // NSURLSession *session = [NSURLSession sharedSession];
-
- // 上传任务
- HLog(@"------------%@",self.pendingUploads);
- __block NSMutableArray *pendingUploads = [self.pendingUploads copy];
-
- NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request
- fromData:requestData
- completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
-
- if (data) {
- NSDictionary *dict = nil;
- NSMutableArray *uploaded = [NSMutableArray array];
- dict = [NSJSONSerialization JSONObjectWithData:data
- options:NSJSONReadingMutableLeaves
- error:nil];
- NSArray *results = [[dict objectForKey:@"result"] componentsSeparatedByString:@";"];
-
- // 已上传成功被返回的文件名
- for (NSString *name in results) {
- if (name.lastPathComponent.length > 37) {
- name = [name.lastPathComponent substringFromIndex:37];
- }
- else {
- name = name.lastPathComponent;
- }
-
- for (NSString *filePath in pendingUploads) {
-
- if ([filePath.lastPathComponent isEqualToString:name]) {
- [uploaded addObject:filePath];
- break;
- }
- else {
- continue;
- }
- }
- }
-
- // 删除本地的文件
- for (NSString *path in uploaded) {
- [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
- }
-
- HLog(@"-------------data:%@", dict);
- HLog(@"-------------uploaded:%@", uploaded);
- HLog(@"-------------response:%@", response);
- HLog(@"-------------error:%@", error);
- }
- }];
-
- [task resume];
- }
- - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
-
- NSArray *domains = [[Domains sharedInstance] domains];
-
- if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
- HLog(@"challenge.protectionSpace.host:%@", challenge.protectionSpace.host);
- if([domains containsObject:challenge.protectionSpace.host]) {
- NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
- completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
- }
- }
- }
- /**
- 获取应用程序的BundleName
- */
- - (NSString *)getBundleName {
- NSString *bundleName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
- if (!bundleName) {
- bundleName = @"Unknown";
- }
- return bundleName;
- }
- /**
- 获取Log日志存储的位置
-
- ~/Library/Caches/VclCrash/JoyLite/Reports/...
-
- @return 日志文件所在路径
- */
- - (NSString *)getBasePath {
- NSArray *directories = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
-
- if (0 == directories.count) {
- HLog(@"Could not locate caches directory path.");
- return nil;
- }
-
- NSString *cachesPath = [directories firstObject];
- if (0 == cachesPath.length) {
- HLog(@"Could not locate caches directory path.");
- return nil;
- }
-
- NSString *path = [@"VclCrash" stringByAppendingPathComponent:[self getBundleName]];
-
- return [cachesPath stringByAppendingPathComponent:path];
- }
- /**
- 获取日志文件存储所在的目录文件路径
- ~/Library/Caches/VclCrash/JoyLite/Reports/...
- */
- - (NSString *)getReportsDirectory {
- return [self.getBasePath stringByAppendingPathComponent:@"Reports"];
- }
- /**
- 获取待上传的文件列表
- */
- - (void)getPendingUploadFiles {
- // 所查找文件夹的路径
- NSString *reportPath = [self getReportsDirectory];
- // 目录迭代器
- NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:reportPath];
- NSMutableArray<NSString *> *array = [NSMutableArray array];
- for (NSString *name in enumerator) {
- if ([name.pathExtension isEqualToString:@"txt"]) {
-
- [array addObject:[[self getReportsDirectory] stringByAppendingPathComponent:name]];
- }
- }
-
- [self.pendingUploads addObjectsFromArray:array];
-
- while ([self.pendingUploads count] > 200)
- {
- NSString *path = [self.pendingUploads firstObject];
-
- [self removeLocalFileWithFullPath:path];
- [self.pendingUploads removeObject:path];
- }
- }
- /**
- 每次崩溃时,信息保存的文件名称
- */
- - (void)createNewReportFile {
- NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
- dateFormatter.dateFormat = @"yyyy.MM.dd_HH.mm.ss.ssss";
- NSString *filename = [NSString stringWithFormat:@"log_%@_ios.txt", [dateFormatter stringFromDate:[NSDate date]]];
- [[NSFileManager defaultManager] createFileAtPath:[[self getReportsDirectory] stringByAppendingPathComponent:filename] contents:nil attributes:nil];
- self.currentReport = [filename copy];
- }
- - (NSString *)createCrashReportFile {
- NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
- dateFormatter.dateFormat = @"yyyy.MM.dd_HH.mm.ss.ssss";
- NSString *filename = [NSString stringWithFormat:@"log_%@_ios_crash.txt", [dateFormatter stringFromDate:[NSDate date]]];
- [[NSFileManager defaultManager] createFileAtPath:[[self getReportsDirectory] stringByAppendingPathComponent:filename] contents:nil attributes:nil];
- return filename;
- }
- - (NSString *)getHttpRequestURLString {
- NSString *result = [HWDataManager getStringWithKey:@"chinaArea"];
- if ([result isEqualToString:@"1"]) { // 国内
- return HTTP_REQUEST_URL_STRING_CN;
- }
- else { // 国外
- NSString *appID = [[NSBundle mainBundle] bundleIdentifier];
- if ([appID isEqualToString:@"com.jjyang.dadaCamera"] ||
- [appID isEqualToString:@"com.dafeng.dadasmartcam"]) { // 大丰项目
- return HTTP_REQUEST_URL_STRING_IO_DF;
- }
- else {
- return HTTP_REQUEST_URL_STRING_IO;
- }
- }
- }
- - (NSMutableArray<NSString *> *)pendingUploads {
- if (!_pendingUploads) {
- _pendingUploads = [NSMutableArray array];
- }
- return _pendingUploads;
- }
- // 类似:2EAA6CEC-6BAC-43B2-84B2-E528D03C01FD,总长36.
- - (NSString *)fetchDeviceUUID {
- NSString *uuid = [[UIDevice currentDevice] identifierForVendor].UUIDString;
- return uuid;
- }
- - (void)uploadCrashReportFiles {
- NSArray *paths = [self.pendingUploads copy];
- NSMutableArray *uploadArray = [[NSMutableArray alloc] init];
- for (NSString *obj in paths) {
- KyoLog(@"文件路径 obj %@",obj);
- if ([obj containsString:@"ios_crash.txt"]) { //只上传奔溃文件的日志,做个文件路径的筛选
- KyoLog(@"崩溃日志文件路径 obj %@",obj);
- [uploadArray addObject:obj];
- }
- }
- if (uploadArray.count == 0) {
- return;
- }
- [self uploadBuryPointData:[NSArray arrayWithArray:uploadArray]];
- }
- - (void)uploadBuryPointData:(NSArray<NSString *> *)filePaths
- {
- NSMutableArray *emptyFileFullPaths = [NSMutableArray array];
- NSMutableArray *uploadFileFullPaths = [NSMutableArray array];
-
- NSMutableArray *dataArray = [[NSMutableArray alloc] init];
- for (NSString *obj in filePaths) {
- NSData *data = [NSData dataWithContentsOfFile:obj];
- NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- NSArray *strARy = [dataStr componentsSeparatedByString:@"__"];
- for (NSString *subStr in strARy) {
- // HLog(@"subStr %@ subStrCount %lu",subStr, (unsigned long)subStr.length);
- if (subStr.length > 0) { //去除换行或者空数据
- BuryPointModel *model = [subStr mj_JSONObject];
- NSDictionary *dic = [model mj_keyValues];
- [dataArray addObject:dic];
- }
- }
- // HLog(@"dataStr %@ strARy %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], strARy);
-
- // 如果是有数据才进行拼接并且上传
- if (data.length) {
- self.isFormDataEmpty = NO;
- [uploadFileFullPaths addObject:obj];
- }
- else { // 否则本地的空文件直接删除
- if ([self.pendingUploads containsObject:obj]) {
- [self.pendingUploads removeObject:obj];
- [emptyFileFullPaths addObject:obj];
- }
- }
- }
-
- // 如果上传的文件都为空,则不进行下方的上传操作
- if (self.isFormDataEmpty) {
- return;
- }
-
- for (NSString *path in emptyFileFullPaths) {
- [self removeLocalFileWithFullPath:path];
- }
-
- for (NSString *path in uploadFileFullPaths) {
- [self removeLocalFileWithFullPath:path];
- }
-
- // [[UseAccountManage shareInstance] extensionPublicToBuryPointPostCallBackCode:PostBurialSiteLogAPI Parameters:dataArray success:^(id _Nonnull responseObject) {
- // SuperModel *mod = [[SuperModel alloc] initWithDictionary:responseObject error:nil];
- // if (mod.status && mod.status.integerValue == 0) {
- // //上传成功后把文件缓存清除
- // for (NSString *path in uploadFileFullPaths) {
- // [self removeLocalFileWithFullPath:path];
- // }
- // }
- // } failure:^(NSError * _Nonnull error) {
- //
- // }];
- }
- @end
|