Classes/TLSFileOutputStream.m (221 lines of code) (raw):
//
// TLSFileOutputStream.m
// TwitterLoggingService
//
// Created on 12/11/13.
// Copyright (c) 2016 Twitter, Inc.
//
// 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 "TLS_Project.h"
#import "TLSFileOutputStream+Protected.h"
static NSString * const TLSFileOutputEventKeyNewLogFilePath = @"newLogFilePath";
@implementation TLSFileOutputStream
#pragma mark - initialization/cleanup
- (instancetype)initWithLogFileDirectoryPath:(NSString*)logFileDirectoryPath
logFileName:(NSString*)logFileName
error:(out NSError **)errorOut
{
if (0 == [logFileDirectoryPath length]) {
if (errorOut) {
*errorOut = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EINVAL
userInfo:@{ @"message" : @"unable to create directory without a name",
@"exceptionName" : NSInvalidArgumentException }];
}
return nil;
}
if (0 == [logFileName length]) {
if (errorOut) {
*errorOut = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EINVAL
userInfo:@{ @"message" : @"unable to create file using empty name",
@"exceptionName" : NSInvalidArgumentException }];
}
return nil;
}
if (![[self class] createLogFileDirectoryAtPath:logFileDirectoryPath error:errorOut]) {
return nil;
}
if (self = [super init]) {
_composeLogMessageOptions = TLSComposeLogMessageInfoDefaultOptions;
if (![self openLogFilePath:[logFileDirectoryPath stringByAppendingPathComponent:logFileName] error:errorOut]) {
return nil;
}
}
return self;
}
- (instancetype)initWithLogFileName:(NSString*)logFileName
error:(out NSError **)errorOut
{
return [self initWithLogFileDirectoryPath:[TLSFileOutputStream defaultLogFileDirectoryPath]
logFileName:logFileName
error:errorOut];
}
- (instancetype)init
{
[self doesNotRecognizeSelector:_cmd];
abort(); // will never be reached, but prevents compiler warning
}
- (void)dealloc
{
if (_logFile) {
fflush(_logFile);
fclose(_logFile);
}
}
#pragma mark - public class method implementations
+ (NSString *)defaultLogFileDirectoryPath
{
static NSString *_defaultLogFileDirectoryPath;
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
@autoreleasepool {
NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
#if !TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
// platform may be non-sandboxed, or "sandbox" may contain sym-links outside of expected sandbox
// ensure unique path using bundle-id or process name for safety (if possible)
NSString *extraPath = [[NSBundle mainBundle] bundleIdentifier] ?: TLSGetProcessBinaryName() ?: @"TLS";
path = [path stringByResolvingSymlinksInPath];
if (![path containsString:[NSString stringWithFormat:@"/%@/", extraPath]]) {
path = [path stringByAppendingPathComponent:extraPath];
}
#endif
_defaultLogFileDirectoryPath = [path stringByAppendingPathComponent:@"logs"];
}
});
return _defaultLogFileDirectoryPath;
}
- (NSStringEncoding)tls_loggedDataEncoding
{
return NSUTF8StringEncoding;
}
- (BOOL)resetAndReturnError:(out NSError * __nullable * __nullable)error
{
if (_logFile) {
fclose(_logFile);
_logFile = NULL;
[[NSFileManager defaultManager] removeItemAtPath:_logFilePath error:NULL];
}
return [self openLogFilePath:_logFilePath error:error];
}
#pragma mark - TLSOutputStream protocol implementation
- (void)tls_flush
{
if (_logFile) {
fflush(_logFile);
}
}
- (void)tls_outputLogInfo:(TLSLogMessageInfo *)logInfo
{
NSString *message = [logInfo composeFormattedMessageWithOptions:self.composeLogMessageOptions];
NSData *messageData = [message dataUsingEncoding:self.tls_loggedDataEncoding];
[self outputLogData:messageData];
}
@end
@implementation TLSFileOutputStream(Protected)
#pragma mark - "protected" method implementations
- (void)writeBytes:(const char*)bytes length:(size_t)length
{
if (_logFile && bytes != NULL && length > 0) {
_bytesWritten += fwrite(bytes, 1, length, _logFile);
if (_flushAfterEveryWriteEnabled) {
fflush(_logFile);
}
}
}
- (void)writeByte:(const char)byte
{
[self writeBytes:&byte length:1];
}
- (void)writeNewline
{
[self writeBytes:"\n" length:1];
}
- (void)writeData:(NSData *)data
{
[self writeBytes:(const char*)data.bytes length:data.length];
}
- (void)writeString:(NSString *)string
{
[self writeData:[string dataUsingEncoding:self.tls_loggedDataEncoding]];
}
+ (BOOL)createDefaultLogFileDirectoryOrError:(out NSError **)errorOut
{
return [self createLogFileDirectoryAtPath:[TLSFileOutputStream defaultLogFileDirectoryPath]
error:errorOut];
}
+ (BOOL)createLogFileDirectoryAtPath:(NSString*)logFileDirectoryPath
error:(out NSError **)errorOut
{
if (0 == [logFileDirectoryPath length]) {
if (errorOut) {
*errorOut = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EINVAL
userInfo:@{ @"message" : @"unable to create directory without a name",
@"exceptionName" : NSInvalidArgumentException }];
}
return NO;
}
NSFileManager* fm = [NSFileManager defaultManager];
NSError* error;
[fm createDirectoryAtPath:logFileDirectoryPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (!error) {
BOOL isDir = NO;
if (![fm fileExistsAtPath:logFileDirectoryPath isDirectory:&isDir]) {
if (errorOut) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:ENOENT
userInfo:@{ @"message" : [NSString stringWithFormat:@"'%@' not created", logFileDirectoryPath],
@"exceptionName" : NSObjectInaccessibleException }];
}
} else if (!isDir) {
if (errorOut) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EEXIST
userInfo:@{ @"message" : [NSString stringWithFormat:@"'%@' already exists, but is not a directory", logFileDirectoryPath],
@"exceptionName" : NSObjectInaccessibleException }];
}
}
}
if (error) {
if (errorOut) {
*errorOut = error;
}
return NO;
}
return YES;
}
- (BOOL)openLogFilePath:(NSString*)logFilePath
error:(NSError **)errorOut
{
if (0 == [logFilePath length]) {
if (errorOut) {
*errorOut = [NSError errorWithDomain:NSPOSIXErrorDomain
code:EINVAL
userInfo:@{ @"message" : @"unable to create file using empty name",
@"exceptionName" : NSInvalidArgumentException }];
}
return NO;
}
FILE *newLogFile = fopen(logFilePath.UTF8String, "w");
if (!newLogFile) {
if (errorOut) {
int errCode = errno;
NSDictionary *info = @{ TLSFileOutputEventKeyNewLogFilePath : (logFilePath) ?: [NSNull null],
@"message" : @"Could not create file for logging to!",
@"exceptionName" : NSObjectInaccessibleException };
*errorOut = [NSError errorWithDomain:NSPOSIXErrorDomain
code:errCode
userInfo:info];
}
return NO;
}
if (_logFile) {
[self tls_flush];
fclose(_logFile);
}
_logFilePath = [logFilePath copy];
_logFileDirectoryPath = [_logFilePath stringByDeletingLastPathComponent];
_logFile = newLogFile;
_bytesWritten = 0;
return YES;
}
- (void)outputLogData:(NSData *)data
{
[self writeData:data];
[self writeNewline];
}
@end