mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
#166: Re-write MXLogger in Swift
* Add logging tests that cover file rotation. * Re-write MXLogger in Swift.
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "MXLogObjcWrapper.h"
|
||||
|
||||
#define MXLogVerbose(message, ...) { \
|
||||
[MXLogObjcWrapper logVerbose:[NSString stringWithFormat: message, ##__VA_ARGS__] file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__]; \
|
||||
}
|
||||
|
||||
#define MXLogDebug(message, ...) { \
|
||||
[MXLogObjcWrapper logDebug:[NSString stringWithFormat: message, ##__VA_ARGS__] file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__]; \
|
||||
}
|
||||
|
||||
#define MXLogInfo(message, ...) { \
|
||||
[MXLogObjcWrapper logInfo:[NSString stringWithFormat: message, ##__VA_ARGS__] file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__]; \
|
||||
}
|
||||
|
||||
#define MXLogWarning(message, ...) { \
|
||||
[MXLogObjcWrapper logWarning:[NSString stringWithFormat: message, ##__VA_ARGS__] file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__]; \
|
||||
}
|
||||
|
||||
#define MXLogError(message) { \
|
||||
[MXLogObjcWrapper logError:message file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__ context:nil]; \
|
||||
}
|
||||
|
||||
#define MXLogErrorDetails(message, details) { \
|
||||
[MXLogObjcWrapper logError:message file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__ context:details]; \
|
||||
}
|
||||
|
||||
#define MXLogFailure(message) { \
|
||||
[MXLogObjcWrapper logFailure:message file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__ context:nil]; \
|
||||
}
|
||||
|
||||
#define MXLogFailureDetails(message, details) { \
|
||||
[MXLogObjcWrapper logFailure:message file:@__FILE__ function:[NSString stringWithFormat:@"%s", __FUNCTION__] line:__LINE__ context:details]; \
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
An `MXLog` Objective-C wrapper used to allow calling Swift methods from the variadic macros defined in `MXLog.h`
|
||||
*/
|
||||
@interface MXLogObjcWrapper : NSObject
|
||||
|
||||
+ (void)logVerbose:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line;
|
||||
|
||||
+ (void)logDebug:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line;
|
||||
|
||||
+ (void)logInfo:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line;
|
||||
|
||||
+ (void)logWarning:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line;
|
||||
|
||||
+ (void)logError:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line context:(nullable id)context;
|
||||
|
||||
+ (void)logFailure:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line context:(nullable id)context;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,52 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "MXLogObjcWrapper.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
@implementation MXLogObjcWrapper
|
||||
|
||||
+ (void)logVerbose:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line
|
||||
{
|
||||
[MXLog logVerbose:message file:file function:function line:line];
|
||||
}
|
||||
|
||||
+ (void)logDebug:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line
|
||||
{
|
||||
[MXLog logDebug:message file:file function:function line:line];
|
||||
}
|
||||
|
||||
+ (void)logInfo:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line
|
||||
{
|
||||
[MXLog logInfo:message file:file function:function line:line];
|
||||
}
|
||||
|
||||
+ (void)logWarning:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line
|
||||
{
|
||||
[MXLog logWarning:message file:file function:function line:line];
|
||||
}
|
||||
|
||||
+ (void)logError:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line context:(id)context
|
||||
{
|
||||
[MXLog logError:message file:file function:function line:line context:context];
|
||||
}
|
||||
|
||||
+ (void)logFailure:(NSString *)message file:(NSString *)file function:(NSString *)function line:(NSUInteger)line context:(id)context
|
||||
{
|
||||
[MXLog logFailure:message file:file function:function line:line context:context];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
The `MXLogger` tool redirects NSLog output into a fixed pool of files.
|
||||
Another log file is used every time [MXLogger redirectNSLogToFiles:YES]
|
||||
is called. The pool contains 3 files.
|
||||
|
||||
`MXLogger` can track and log uncatched exceptions or crashes.
|
||||
*/
|
||||
@interface MXLogger : NSObject
|
||||
|
||||
/**
|
||||
Redirect NSLog output to MXLogger files.
|
||||
|
||||
It is advised to condition this redirection in '#if (!isatty(STDERR_FILENO))' block to enable
|
||||
it only when the device is not attached to the debugger.
|
||||
|
||||
@param redirectNSLogToFiles YES to enable the redirection.
|
||||
*/
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles;
|
||||
|
||||
/**
|
||||
Redirect NSLog output to MXLogger files.
|
||||
|
||||
It is advised to condition this redirection in '#if (!isatty(STDERR_FILENO))' block to enable
|
||||
it only when the device is not attached to the debugger.
|
||||
|
||||
@param redirectNSLogToFiles YES to enable the redirection.
|
||||
@param numberOfFiles number of files to keep (default is 10).
|
||||
*/
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles;
|
||||
|
||||
/**
|
||||
Redirect NSLog output to MXLogger files.
|
||||
|
||||
@param redirectNSLogToFiles YES to enable the redirection.
|
||||
@param numberOfFiles number of files to keep (default is 10).
|
||||
@param sizeLimit size limit of log files in bytes. 0 means no limitation, the default value for other methods
|
||||
*/
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles sizeLimit:(NSUInteger)sizeLimit;
|
||||
|
||||
/**
|
||||
Delete all log files.
|
||||
*/
|
||||
+ (void)deleteLogFiles;
|
||||
|
||||
/**
|
||||
Get the list of all log files.
|
||||
|
||||
@return files of
|
||||
*/
|
||||
+ (NSArray<NSString*>*)logFiles;
|
||||
|
||||
/**
|
||||
Make `MXLogger` catch and log unmanaged exceptions or application crashes.
|
||||
|
||||
When such error happens, `MXLogger` stores the application stack trace into a file
|
||||
just before the application leaves. The path of this file is provided by [MXLogger crashLog].
|
||||
|
||||
@param logCrashes YES to enable the catch.
|
||||
*/
|
||||
+ (void)logCrashes:(BOOL)logCrashes;
|
||||
|
||||
/**
|
||||
Set the app build version.
|
||||
It will be reported in crash report.
|
||||
*/
|
||||
+ (void)setBuildVersion:(NSString*)buildVersion;
|
||||
|
||||
/**
|
||||
Set a sub name for namespacing log files.
|
||||
|
||||
A sub name must be set when running from an app extension because extensions can
|
||||
run in parallel to the app.
|
||||
It must be called before `redirectNSLogToFiles`.
|
||||
|
||||
@param subLogName the subname for log files. Files will be named as 'console-[subLogName].log'
|
||||
Default is nil.
|
||||
*/
|
||||
+ (void)setSubLogName:(NSString*)subLogName;
|
||||
|
||||
/**
|
||||
If any, get the file containing the last application crash log.
|
||||
|
||||
Only one crash log is stored at a time. The best moment for the app to handle it is the
|
||||
at its next startup.
|
||||
|
||||
@return the crash log file. nil if there is none.
|
||||
*/
|
||||
+ (NSString*)crashLog;
|
||||
|
||||
/**
|
||||
Delete the crash log file.
|
||||
*/
|
||||
+ (void)deleteCrashLog;
|
||||
|
||||
@end
|
||||
@@ -1,392 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#import "MXLogger.h"
|
||||
#import "MXLog.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
// stderr so it can be restored
|
||||
int stderrSave = 0;
|
||||
|
||||
static NSString *buildVersion;
|
||||
static NSString *subLogName;
|
||||
|
||||
#define MXLOGGER_CRASH_LOG @"crash.log"
|
||||
|
||||
@implementation MXLogger
|
||||
|
||||
#pragma mark - NSLog redirection
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles
|
||||
{
|
||||
[self redirectNSLogToFiles:redirectNSLogToFiles numberOfFiles:10];
|
||||
}
|
||||
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles
|
||||
{
|
||||
[self redirectNSLogToFiles:redirectNSLogToFiles numberOfFiles:numberOfFiles sizeLimit:0];
|
||||
}
|
||||
|
||||
+ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles sizeLimit:(NSUInteger)sizeLimit
|
||||
{
|
||||
if (redirectNSLogToFiles)
|
||||
{
|
||||
NSMutableString *log = [NSMutableString string];
|
||||
|
||||
// Default subname
|
||||
if (!subLogName)
|
||||
{
|
||||
subLogName = @"";
|
||||
}
|
||||
|
||||
// Set log location
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *logsFolderPath = [MXLogger logsFolderPath];
|
||||
|
||||
// Do a circular buffer based on X files
|
||||
for (NSInteger index = numberOfFiles - 2; index >= 0; index--)
|
||||
{
|
||||
NSString *nsLogPathOlder;
|
||||
NSString *nsLogPathCurrent;
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
nsLogPathOlder = [NSString stringWithFormat:@"console%@.1.log", subLogName];
|
||||
nsLogPathCurrent = [NSString stringWithFormat:@"console%@.log", subLogName];
|
||||
}
|
||||
else
|
||||
{
|
||||
nsLogPathOlder = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index + 1];
|
||||
nsLogPathCurrent = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index];
|
||||
}
|
||||
|
||||
nsLogPathOlder = [logsFolderPath stringByAppendingPathComponent:nsLogPathOlder];
|
||||
nsLogPathCurrent = [logsFolderPath stringByAppendingPathComponent:nsLogPathCurrent];
|
||||
|
||||
if ([fileManager fileExistsAtPath:nsLogPathCurrent])
|
||||
{
|
||||
if ([fileManager fileExistsAtPath:nsLogPathOlder])
|
||||
{
|
||||
// Temp log
|
||||
[log appendFormat:@"removeItemAtPath: %@\n", nsLogPathOlder];
|
||||
|
||||
NSError *error;
|
||||
[fileManager removeItemAtPath:nsLogPathOlder error:&error];
|
||||
if (error)
|
||||
{
|
||||
[log appendFormat:@"removeItemAtPath: %@. Error: %@\n", nsLogPathOlder, error];
|
||||
}
|
||||
}
|
||||
|
||||
// Temp log
|
||||
[log appendFormat:@"moveItemAtPath: %@ toPath: %@\n", nsLogPathCurrent, nsLogPathOlder];
|
||||
|
||||
NSError *error;
|
||||
[fileManager moveItemAtPath:nsLogPathCurrent toPath:nsLogPathOlder error:&error];
|
||||
if (error)
|
||||
{
|
||||
[log appendFormat:@"moveItemAtPath: %@ toPath: %@. Error: %@\n", nsLogPathCurrent, nsLogPathOlder, error];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save stderr so it can be restored.
|
||||
stderrSave = dup(STDERR_FILENO);
|
||||
|
||||
NSString *nsLogPath = [logsFolderPath stringByAppendingPathComponent:[NSString stringWithFormat:@"console%@.log", subLogName]];
|
||||
freopen([nsLogPath fileSystemRepresentation], "w+", stderr);
|
||||
|
||||
MXLogDebug(@"redirectNSLogToFiles: YES");
|
||||
if (log.length)
|
||||
{
|
||||
// We can now log into files
|
||||
MXLogDebug(@"%@", log);
|
||||
}
|
||||
|
||||
[self removeExtraFilesFromCount:numberOfFiles];
|
||||
|
||||
if (sizeLimit > 0)
|
||||
{
|
||||
[self removeFilesAfterSizeLimit:sizeLimit];
|
||||
}
|
||||
}
|
||||
else if (stderrSave)
|
||||
{
|
||||
// Flush before restoring stderr
|
||||
fflush(stderr);
|
||||
|
||||
// Now restore stderr, so new output goes to console.
|
||||
dup2(stderrSave, STDERR_FILENO);
|
||||
close(stderrSave);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)deleteLogFiles
|
||||
{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
for (NSString *logFile in [self logFiles])
|
||||
{
|
||||
[fileManager removeItemAtPath:logFile error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<NSString*>*)logFiles
|
||||
{
|
||||
NSMutableArray *logFiles = [NSMutableArray array];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *logsFolderPath = [MXLogger logsFolderPath];
|
||||
|
||||
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:logsFolderPath];
|
||||
|
||||
// Find all *.log files
|
||||
NSString *file = nil;
|
||||
while ((file = [dirEnum nextObject]))
|
||||
{
|
||||
if ([[file lastPathComponent] hasPrefix:@"console"])
|
||||
{
|
||||
NSString *logPath = [logsFolderPath stringByAppendingPathComponent:file];
|
||||
[logFiles addObject:logPath];
|
||||
}
|
||||
}
|
||||
|
||||
MXLogDebug(@"logFiles: %@", logFiles);
|
||||
|
||||
return logFiles;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Exceptions and crashes
|
||||
// Exceptions uncaught by try catch block are handled here
|
||||
static void handleUncaughtException(NSException *exception)
|
||||
{
|
||||
[MXLogger logCrashes:NO];
|
||||
|
||||
// Extract running app information
|
||||
NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary];
|
||||
NSString* appVersion;
|
||||
NSString* app, *appId;
|
||||
|
||||
app = infoDict[@"CFBundleExecutable"];
|
||||
appId = infoDict[@"CFBundleIdentifier"];
|
||||
|
||||
if ([infoDict objectForKey:@"CFBundleVersion"])
|
||||
{
|
||||
appVersion = [NSString stringWithFormat:@"%@ (r%@)", [infoDict objectForKey:@"CFBundleShortVersionString"], [infoDict objectForKey:@"CFBundleVersion"]];
|
||||
}
|
||||
else
|
||||
{
|
||||
appVersion = [infoDict objectForKey:@"CFBundleShortVersionString"];
|
||||
}
|
||||
|
||||
// Build the crash log
|
||||
#if TARGET_OS_IPHONE
|
||||
NSString *model = [[UIDevice currentDevice] model];
|
||||
NSString *version = [[UIDevice currentDevice] systemVersion];
|
||||
#elif TARGET_OS_OSX
|
||||
NSString *model = @"Mac";
|
||||
NSString *version = [[NSProcessInfo processInfo] operatingSystemVersionString];
|
||||
#endif
|
||||
NSArray *backtrace = [exception callStackSymbols];
|
||||
NSString *description = [NSString stringWithFormat:@"%.0f - %@\n%@\nApplication: %@ (%@)\nApplication version: %@\nBuild: %@\n%@ %@\n\nMain thread: %@\n%@\n",
|
||||
[[NSDate date] timeIntervalSince1970],
|
||||
[NSDate date],
|
||||
exception.description,
|
||||
app, appId,
|
||||
appVersion,
|
||||
buildVersion,
|
||||
model, version,
|
||||
[NSThread isMainThread] ? @"YES" : @"NO",
|
||||
backtrace];
|
||||
|
||||
// Write to the crash log file
|
||||
[MXLogger deleteCrashLog];
|
||||
NSString *crashLog = crashLogPath();
|
||||
[description writeToFile:crashLog
|
||||
atomically:NO
|
||||
encoding:NSStringEncodingConversionAllowLossy
|
||||
error:nil];
|
||||
|
||||
MXLogErrorDetails(@"handleUncaughtException", @{
|
||||
@"description": description ?: @"unknown"
|
||||
});
|
||||
}
|
||||
|
||||
// Signals emitted by the app are handled here
|
||||
static void handleSignal(int signalValue)
|
||||
{
|
||||
// Throw a custom Objective-C exception
|
||||
// The Objective-C runtime will then be able to build a readable call stack in handleUncaughtException
|
||||
[NSException raise:@"Signal detected" format:@"Signal detected: %d", signalValue];
|
||||
}
|
||||
|
||||
+ (void)logCrashes:(BOOL)logCrashes
|
||||
{
|
||||
if (logCrashes)
|
||||
{
|
||||
// Handle not managed exceptions by ourselves
|
||||
NSSetUncaughtExceptionHandler(&handleUncaughtException);
|
||||
|
||||
// Register signal event (seg fault & cie)
|
||||
signal(SIGABRT, handleSignal);
|
||||
signal(SIGILL, handleSignal);
|
||||
signal(SIGSEGV, handleSignal);
|
||||
signal(SIGFPE, handleSignal);
|
||||
signal(SIGBUS, handleSignal);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Disable crash handling
|
||||
NSSetUncaughtExceptionHandler(NULL);
|
||||
signal(SIGABRT, SIG_DFL);
|
||||
signal(SIGILL, SIG_DFL);
|
||||
signal(SIGSEGV, SIG_DFL);
|
||||
signal(SIGFPE, SIG_DFL);
|
||||
signal(SIGBUS, SIG_DFL);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)setBuildVersion:(NSString *)theBuildVersion
|
||||
{
|
||||
buildVersion = theBuildVersion;
|
||||
}
|
||||
|
||||
+ (void)setSubLogName:(NSString *)theSubLogName
|
||||
{
|
||||
subLogName = [NSString stringWithFormat:@"-%@", theSubLogName];
|
||||
}
|
||||
|
||||
// Return the path of the crash log file
|
||||
static NSString* crashLogPath(void)
|
||||
{
|
||||
return [[MXLogger logsFolderPath] stringByAppendingPathComponent:MXLOGGER_CRASH_LOG];
|
||||
}
|
||||
|
||||
+ (NSString*)crashLog
|
||||
{
|
||||
NSString *exceptionLog;
|
||||
|
||||
NSString *crashLog = crashLogPath();
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if([fileManager fileExistsAtPath:crashLog])
|
||||
{
|
||||
exceptionLog = crashLog;
|
||||
}
|
||||
return exceptionLog;
|
||||
}
|
||||
|
||||
+ (void)deleteCrashLog
|
||||
{
|
||||
NSString *crashLog = crashLogPath();
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if([fileManager fileExistsAtPath:crashLog])
|
||||
{
|
||||
[fileManager removeItemAtPath:crashLog error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
// The folder where logs are stored
|
||||
+ (NSString*)logsFolderPath
|
||||
{
|
||||
NSString *logsFolderPath = nil;
|
||||
|
||||
NSURL *sharedContainerURL = [[NSFileManager defaultManager] appGroupContainerURL];
|
||||
if (sharedContainerURL)
|
||||
{
|
||||
logsFolderPath = [sharedContainerURL path];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSArray<NSURL *> *paths = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
||||
logsFolderPath = paths[0].path;
|
||||
}
|
||||
|
||||
return logsFolderPath;
|
||||
}
|
||||
|
||||
|
||||
// If [self redirectNSLogToFiles: numberOfFiles:] is called with a lower numberOfFiles we need to do some cleanup
|
||||
+ (void)removeExtraFilesFromCount:(NSUInteger)count
|
||||
{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *logsFolderPath = [MXLogger logsFolderPath];
|
||||
|
||||
NSUInteger index = count;
|
||||
do
|
||||
{
|
||||
NSString *fileName = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index];
|
||||
NSString *logFile = [logsFolderPath stringByAppendingPathComponent:fileName];
|
||||
|
||||
if ([fileManager fileExistsAtPath:logFile])
|
||||
{
|
||||
[fileManager removeItemAtPath:logFile error:nil];
|
||||
MXLogDebug(@"removeExtraFilesFromCount: %@. removeItemAtPath: %@\n", @(count), logFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (index++);
|
||||
}
|
||||
|
||||
+ (void)removeFilesAfterSizeLimit:(NSUInteger)sizeLimit
|
||||
{
|
||||
NSUInteger logSize = 0;
|
||||
BOOL removeFiles = NO;
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *logsFolderPath = [MXLogger logsFolderPath];
|
||||
|
||||
// Start from console.1.log. Do not consider console.log. It should be almost empty
|
||||
NSUInteger index = 0;
|
||||
while (++index)
|
||||
{
|
||||
NSString *fileName = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index];
|
||||
NSString *logFile = [logsFolderPath stringByAppendingPathComponent:fileName];
|
||||
|
||||
if ([fileManager fileExistsAtPath:logFile])
|
||||
{
|
||||
logSize += [fileManager attributesOfItemAtPath:logFile error:nil].fileSize;
|
||||
|
||||
if (logSize >= sizeLimit)
|
||||
{
|
||||
removeFiles = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (removeFiles)
|
||||
{
|
||||
MXLogDebug(@"removeFilesAfterSizeLimit: Remove files from index %@ because logs are too large (%@ for a limit of %@)\n",
|
||||
@(index),
|
||||
[NSByteCountFormatter stringFromByteCount:logSize countStyle:NSByteCountFormatterCountStyleBinary],
|
||||
[NSByteCountFormatter stringFromByteCount:sizeLimit countStyle:NSByteCountFormatterCountStyleBinary]);
|
||||
[self removeExtraFilesFromCount:index];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"removeFilesAfterSizeLimit: No need: %@ for a limit of %@\n",
|
||||
[NSByteCountFormatter stringFromByteCount:logSize countStyle:NSByteCountFormatterCountStyleBinary],
|
||||
[NSByteCountFormatter stringFromByteCount:sizeLimit countStyle:NSByteCountFormatterCountStyleBinary]);
|
||||
}
|
||||
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,327 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// The `MXLogger` tool redirects NSLog output into a fixed pool of files.
|
||||
/// Another log file is used every time `MXLogger redirectNSLog(toFiles: true)`
|
||||
/// is called. The pool contains 3 files.
|
||||
///
|
||||
/// `MXLogger` can track and log uncaught exceptions or crashes.
|
||||
class MXLogger {
|
||||
/// stderr so it can be restored.
|
||||
static var stderrSave: Int32 = 0
|
||||
|
||||
private enum Constants {
|
||||
/// The filename used for the crash log.
|
||||
static let crashLogFileName = "crash.log"
|
||||
}
|
||||
|
||||
/// Redirect NSLog output to MXLogger files.
|
||||
///
|
||||
/// It is advised to condition this redirection in `#if (!isatty(STDERR_FILENO))` block to enable
|
||||
/// it only when the device is not attached to the debugger.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - redirectToFiles: `true` to enable the redirection.
|
||||
/// - numberOfFiles: number of files to keep (default is 10).
|
||||
/// - sizeLimit: size limit of log files in bytes. 0 means no limitation, the default value for other methods
|
||||
static func redirectNSLog(toFiles redirectToFiles: Bool, numberOfFiles: UInt = 10, sizeLimit: UInt = 0) {
|
||||
if redirectToFiles {
|
||||
var tempLog = ""
|
||||
|
||||
// Do a circular buffer based on X files
|
||||
for index in (0...(numberOfFiles - 2)).reversed() {
|
||||
rotateLog(at: index, tempLog: &tempLog)
|
||||
}
|
||||
|
||||
// Save stderr so it can be restored.
|
||||
stderrSave = dup(STDERR_FILENO)
|
||||
|
||||
let nsLogURL = logURL(for: "console\(subLogName).log")
|
||||
freopen((nsLogURL as NSURL).fileSystemRepresentation, "w+", stderr)
|
||||
|
||||
MXLog.debug("redirectNSLogToFiles: true")
|
||||
if !tempLog.isEmpty {
|
||||
// We can now log into files
|
||||
MXLog.debug(tempLog)
|
||||
}
|
||||
|
||||
removeExtraFiles(from: numberOfFiles)
|
||||
|
||||
if sizeLimit > 0 {
|
||||
removeFiles(after: sizeLimit)
|
||||
}
|
||||
} else if stderrSave > 0 {
|
||||
// Flush before restoring stderr
|
||||
fflush(stderr)
|
||||
|
||||
// Now restore stderr, so new output goes to console.
|
||||
dup2(stderrSave, STDERR_FILENO)
|
||||
close(stderrSave)
|
||||
}
|
||||
}
|
||||
|
||||
private static func rotateLog(at index: UInt, tempLog: inout String) {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
let currentURL: URL
|
||||
let newURL: URL
|
||||
|
||||
if index == 0 {
|
||||
currentURL = logURL(for: String("console\(subLogName).log"))
|
||||
newURL = logURL(for: String("console\(subLogName).1.log"))
|
||||
} else {
|
||||
currentURL = logURL(for: String("console\(subLogName).\(index).log"))
|
||||
newURL = logURL(for: String("console\(subLogName).\(index + 1).log"))
|
||||
}
|
||||
|
||||
guard fileManager.fileExists(atPath: currentURL.path()) else { return }
|
||||
|
||||
if fileManager.fileExists(atPath: newURL.path()) {
|
||||
// Temp log
|
||||
tempLog.append("removeItemAt: \(newURL)\n")
|
||||
|
||||
do {
|
||||
try fileManager.removeItem(at: newURL)
|
||||
} catch {
|
||||
tempLog.append("removeItemAt: \(newURL). Error: \(error)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Temp log
|
||||
tempLog.append("moveItemAt: \(currentURL) to: \(newURL)\n")
|
||||
|
||||
do {
|
||||
try fileManager.moveItem(at: currentURL, to: newURL)
|
||||
} catch {
|
||||
tempLog.append("moveItemAt: \(currentURL) to: \(newURL). Error: \(error)\n")
|
||||
}
|
||||
}
|
||||
|
||||
private static func logURL(for fileName: String) -> URL {
|
||||
MXLogger.logsFolderURL.appending(path: fileName)
|
||||
}
|
||||
|
||||
/// Delete all log files.
|
||||
static func deleteLogFiles() {
|
||||
let fileManager = FileManager.default
|
||||
for logFileURL in logFiles {
|
||||
try? fileManager.removeItem(at: logFileURL)
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of all log file URLs.
|
||||
static var logFiles: [URL] {
|
||||
var logFiles = [URL]()
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let enumerator = fileManager.enumerator(at: logsFolderURL, includingPropertiesForKeys: nil)
|
||||
|
||||
// Find all *.log files
|
||||
while let logURL = enumerator?.nextObject() as? URL {
|
||||
if logURL.lastPathComponent.hasPrefix("console") {
|
||||
logFiles.append(logURL)
|
||||
}
|
||||
}
|
||||
|
||||
MXLog.debug("logFiles: \(logFiles)")
|
||||
|
||||
return logFiles
|
||||
}
|
||||
|
||||
// MARK: - Exceptions and crashes
|
||||
|
||||
/// Exceptions uncaught by try catch block are handled here
|
||||
static func handleUncaughtException(_ exception: NSException) {
|
||||
MXLogger.logCrashes(false)
|
||||
|
||||
// Extract running app information
|
||||
let app = ElementInfoPlist.cfBundleExecutable
|
||||
let appId = ElementInfoPlist.cfBundleIdentifier
|
||||
let appVersion = "\(ElementInfoPlist.cfBundleShortVersionString) (r\(ElementInfoPlist.cfBundleVersion))"
|
||||
|
||||
// Build the crash log
|
||||
let model = UIDevice.current.model
|
||||
let version = UIDevice.current.systemVersion
|
||||
|
||||
let backtrace = exception.callStackSymbols
|
||||
let description = String(format: "%.0f - %@\n%@\nApplication: %@ (%@)\nApplication version: %@\nBuild: %@\n%@ %@\n\nMain thread: %@\n%@\n",
|
||||
Date.now.timeIntervalSince1970,
|
||||
NSDate(),
|
||||
exception.description,
|
||||
app, appId,
|
||||
appVersion,
|
||||
buildVersion ?? "Unknown",
|
||||
model, version,
|
||||
Thread.isMainThread ? "true" : "false",
|
||||
backtrace)
|
||||
|
||||
// Write to the crash log file
|
||||
MXLogger.deleteCrashLog()
|
||||
let crashLog = crashLogURL
|
||||
try? description.write(to: crashLog, atomically: false, encoding: .utf8)
|
||||
|
||||
MXLog.error("handleUncaughtException", context: ["description": description])
|
||||
}
|
||||
|
||||
// Signals emitted by the app are handled here
|
||||
private static func handleSignal(_ signalValue: Int32) {
|
||||
// Throw a custom Objective-C exception
|
||||
// The Objective-C runtime will then be able to build a readable call stack in handleUncaughtException
|
||||
withVaList([signalValue]) { NSException.raise(.init("Signal detected"), format: "Signal detected: %d", arguments: $0) }
|
||||
}
|
||||
|
||||
/// Make `MXLogger` catch and log unmanaged exceptions or application crashes.
|
||||
///
|
||||
/// When such error happens, `MXLogger` stores the application stack trace into a file
|
||||
/// just before the application leaves. The path of this file is provided by `MXLogger.crashLog`.
|
||||
///
|
||||
/// - Parameter enabled: `true` to enable the catch.
|
||||
static func logCrashes(_ enabled: Bool) {
|
||||
if enabled {
|
||||
// Handle not managed exceptions by ourselves
|
||||
NSSetUncaughtExceptionHandler { exception in
|
||||
MXLogger.handleUncaughtException(exception)
|
||||
}
|
||||
|
||||
// Register signal event (seg fault & cie)
|
||||
signal(SIGABRT) { MXLogger.handleSignal($0) }
|
||||
signal(SIGILL) { MXLogger.handleSignal($0) }
|
||||
signal(SIGSEGV) { MXLogger.handleSignal($0) }
|
||||
signal(SIGFPE) { MXLogger.handleSignal($0) }
|
||||
signal(SIGBUS) { MXLogger.handleSignal($0) }
|
||||
signal(SIGABRT) { MXLogger.handleSignal($0) }
|
||||
} else {
|
||||
// Disable crash handling
|
||||
NSSetUncaughtExceptionHandler(nil)
|
||||
signal(SIGABRT, SIG_DFL)
|
||||
signal(SIGILL, SIG_DFL)
|
||||
signal(SIGSEGV, SIG_DFL)
|
||||
signal(SIGFPE, SIG_DFL)
|
||||
signal(SIGBUS, SIG_DFL)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the app build version.
|
||||
/// It will be reported in crash report.
|
||||
static var buildVersion: String?
|
||||
|
||||
/// Set a sub name for namespacing log files.
|
||||
///
|
||||
/// A sub name must be set when running from an app extension because extensions can
|
||||
/// run in parallel to the app.
|
||||
/// It must be called before `redirectNSLog(toFiles)`.
|
||||
///
|
||||
/// - Parameter name: the subname for log files. Files will be named as `console-[subLogName].log`
|
||||
/// Default is nil.
|
||||
static func setSubLogName(_ name: String) {
|
||||
if name.isEmpty {
|
||||
subLogName = ""
|
||||
} else {
|
||||
subLogName = "-\(name)"
|
||||
}
|
||||
}
|
||||
|
||||
private static var subLogName = ""
|
||||
|
||||
/// The URL used for a crash log file.
|
||||
static var crashLogURL: URL {
|
||||
MXLogger.logsFolderURL.appending(path: Constants.crashLogFileName)
|
||||
}
|
||||
|
||||
/// The URL of the file containing the last application crash if one exists or `nil` if there is none.
|
||||
///
|
||||
/// Only one crash log is stored at a time. The best moment for the app to handle it is the
|
||||
/// at its next startup.
|
||||
static var crashLog: URL? {
|
||||
let crashLogURL = MXLogger.crashLogURL
|
||||
let fileManager = FileManager.default
|
||||
guard fileManager.fileExists(atPath: crashLogURL.path()) else { return nil }
|
||||
|
||||
return crashLogURL
|
||||
}
|
||||
|
||||
/// Delete the crash log file.
|
||||
static func deleteCrashLog() {
|
||||
let crashLog = MXLogger.crashLogURL
|
||||
let fileManager = FileManager.default
|
||||
|
||||
if fileManager.fileExists(atPath: crashLog.path()) {
|
||||
try? fileManager.removeItem(at: crashLog)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// The folder where logs are stored
|
||||
private static var logsFolderURL: URL {
|
||||
FileManager.default.appGroupContainerURL ?? URL.documentsDirectory
|
||||
}
|
||||
|
||||
/// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup.
|
||||
private static func removeExtraFiles(from count: UInt) {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
for index in count... {
|
||||
let fileName = "console\(subLogName).\(index).log"
|
||||
let logFile = logURL(for: fileName)
|
||||
|
||||
if fileManager.fileExists(atPath: logFile.path()) {
|
||||
try? fileManager.removeItem(at: logFile)
|
||||
MXLog.debug("removeExtraFilesFromCount: \(count). removeItemAt: \(logFile)\n")
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If `redirectNSLog(toFiles:sizeLimit:)` is called with a size limit, we may need to do some cleanup.
|
||||
private static func removeFiles(after sizeLimit: UInt) {
|
||||
var logSize: UInt = 0
|
||||
var indexExceedingSizeLimit: Int?
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Start from console.1.log. Do not consider console.log. It should be almost empty
|
||||
for index in 1... {
|
||||
let fileName = "console\(subLogName).\(index).log"
|
||||
let logFile = logURL(for: fileName)
|
||||
|
||||
if fileManager.fileExists(atPath: logFile.path()) {
|
||||
if let attributes = try? fileManager.attributesOfItem(atPath: logFile.path()), let fileSize = attributes[.size] as? UInt {
|
||||
logSize += fileSize
|
||||
}
|
||||
|
||||
if logSize >= sizeLimit {
|
||||
indexExceedingSizeLimit = index
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let logSizeString = logSize.formatted(.byteCount(style: .binary))
|
||||
let sizeLimitString = sizeLimit.formatted(.byteCount(style: .binary))
|
||||
|
||||
if let indexExceedingSizeLimit {
|
||||
MXLog.debug("removeFilesAfterSizeLimit: Remove files from index \(indexExceedingSizeLimit) because logs are too large (\(logSizeString) for a limit of \(sizeLimitString)\n")
|
||||
removeExtraFiles(from: UInt(indexExceedingSizeLimit))
|
||||
} else {
|
||||
MXLog.debug("removeFilesAfterSizeLimit: No need: \(logSizeString) for a limit of \(sizeLimitString)\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class BugReportService: BugReportServiceProtocol {
|
||||
// also enable logging crashes, to send them with bug reports
|
||||
MXLogger.logCrashes(true)
|
||||
// set build version for logger
|
||||
MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
|
||||
MXLogger.buildVersion = ElementInfoPlist.cfBundleShortVersionString
|
||||
}
|
||||
|
||||
// MARK: - BugReportServiceProtocol
|
||||
@@ -180,12 +180,11 @@ class BugReportService: BugReportServiceProtocol {
|
||||
MXLog.debug("zipFiles: includeLogs: \(includeLogs), includeCrashLog: \(includeCrashLog)")
|
||||
|
||||
var filesToCompress: [URL] = []
|
||||
if includeLogs, let logFiles = MXLogger.logFiles() {
|
||||
let urls = logFiles.compactMap { URL(fileURLWithPath: $0) }
|
||||
filesToCompress.append(contentsOf: urls)
|
||||
if includeLogs {
|
||||
filesToCompress.append(contentsOf: MXLogger.logFiles)
|
||||
}
|
||||
if includeCrashLog, let crashLogFile = MXLogger.crashLog() {
|
||||
filesToCompress.append(URL(fileURLWithPath: crashLogFile))
|
||||
if includeCrashLog, let crashLogFile = MXLogger.crashLog {
|
||||
filesToCompress.append(crashLogFile)
|
||||
}
|
||||
|
||||
var totalSize = 0
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct MockClientProxy: ClientProxyProtocol {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct MockRoomProxy: RoomProxyProtocol {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
private class RoomTimelineListener: TimelineListener {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
/// A light wrapper around timeline items returned from Rust.
|
||||
|
||||
Reference in New Issue
Block a user