[go: nahoru, domu]

blob: da66114c1af5e413072be1eb6a649dc38ef96b8c [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/sessions/session_service_ios.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/format_macros.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/location.h"
#import "base/logging.h"
#import "base/memory/ref_counted.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "base/time/time.h"
#import "ios/chrome/browser/sessions/session_ios.h"
#import "ios/chrome/browser/sessions/session_ios_factory.h"
#import "ios/chrome/browser/sessions/session_window_ios.h"
#import "ios/web/public/session/crw_navigation_item_storage.h"
#import "ios/web/public/session/crw_session_certificate_policy_cache_storage.h"
#import "ios/web/public/session/crw_session_storage.h"
namespace {
const NSTimeInterval kSaveDelay = 2.5; // Value taken from Desktop Chrome.
NSString* const kRootObjectKey = @"root"; // Key for the root object.
// Directory containing session files.
const base::FilePath::CharType kSessions[] = FILE_PATH_LITERAL("Sessions");
// Name of the file storing the list of tabs.
const base::FilePath::CharType kSessionFileName[] =
FILE_PATH_LITERAL("session.plist");
}
@implementation NSKeyedUnarchiver (CrLegacySessionCompatibility)
// When adding a new compatibility alias here, create a new crbug to track its
// removal and mark it with a release at least one year after the introduction
// of the alias.
- (void)cr_registerCompatibilityAliases {
}
@end
@implementation SessionServiceIOS {
// The SequencedTaskRunner on which File IO operations are performed.
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
// Maps session path to the pending session factories for the delayed save
// behaviour. SessionIOSFactory pointers are weak.
NSMapTable<NSString*, SessionIOSFactory*>* _pendingSessions;
}
#pragma mark - NSObject overrides
- (instancetype)init {
scoped_refptr<base::SequencedTaskRunner> taskRunner =
base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::SingleThreadTaskRunnerThreadMode::DEDICATED);
return [self initWithTaskRunner:taskRunner];
}
#pragma mark - Public interface
+ (SessionServiceIOS*)sharedService {
static SessionServiceIOS* singleton = nil;
if (!singleton) {
singleton = [[[self class] alloc] init];
}
return singleton;
}
- (instancetype)initWithTaskRunner:
(const scoped_refptr<base::SequencedTaskRunner>&)taskRunner {
DCHECK(taskRunner);
self = [super init];
if (self) {
_pendingSessions = [NSMapTable strongToWeakObjectsMapTable];
_taskRunner = taskRunner;
}
return self;
}
- (void)shutdownWithCompletion:(ProceduralBlock)completion {
_taskRunner->PostTask(FROM_HERE, base::BindOnce(completion));
}
- (void)saveSession:(__weak SessionIOSFactory*)factory
sessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory
immediately:(BOOL)immediately {
NSString* sessionPath = [[self class] sessionPathForSessionID:sessionID
directory:directory];
BOOL hadPendingSession = [_pendingSessions objectForKey:sessionPath] != nil;
[_pendingSessions setObject:factory forKey:sessionPath];
if (immediately) {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSaveToPathInBackground:sessionPath];
} else if (!hadPendingSession) {
// If there wasn't previously a delayed save pending for `sessionPath`,
// enqueue one now.
[self performSelector:@selector(performSaveToPathInBackground:)
withObject:sessionPath
afterDelay:kSaveDelay];
}
}
- (SessionIOS*)loadSessionWithSessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory {
NSString* sessionPath = [[self class] sessionPathForSessionID:sessionID
directory:directory];
base::TimeTicks start_time = base::TimeTicks::Now();
SessionIOS* session = [self loadSessionFromPath:sessionPath];
UmaHistogramTimes("Session.WebStates.ReadFromFileTime",
base::TimeTicks::Now() - start_time);
return session;
}
- (SessionIOS*)loadSessionFromPath:(NSString*)sessionPath {
NSObject<NSCoding>* rootObject = nil;
@try {
NSData* data = [NSData dataWithContentsOfFile:sessionPath];
if (!data)
return nil;
NSError* error = nil;
NSKeyedUnarchiver* unarchiver =
[[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
if (!unarchiver || error) {
DLOG(WARNING) << "Error creating unarchiver, session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([error description]);
return nil;
}
unarchiver.requiresSecureCoding = NO;
// Register compatibility aliases to support legacy saved sessions.
[unarchiver cr_registerCompatibilityAliases];
rootObject = [unarchiver decodeObjectForKey:kRootObjectKey];
} @catch (NSException* exception) {
NOTREACHED() << "Error loading session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([exception reason]);
}
if (!rootObject)
return nil;
// Support for legacy saved session that contained a single SessionWindowIOS
// object as the root object (pre-M-59).
if ([rootObject isKindOfClass:[SessionWindowIOS class]]) {
return [[SessionIOS alloc] initWithWindows:@[
base::apple::ObjCCastStrict<SessionWindowIOS>(rootObject)
]];
}
return base::apple::ObjCCastStrict<SessionIOS>(rootObject);
}
- (void)deleteAllSessionFilesInDirectory:(const base::FilePath&)directory
completion:(base::OnceClosure)callback {
NSString* sessionsDirectory =
base::apple::FilePathToNSString(directory.Append(kSessions));
NSArray<NSString*>* allSessionIDs = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:sessionsDirectory
error:nil];
[self deleteSessions:allSessionIDs
directory:directory
completion:std::move(callback)];
}
- (void)deleteSessions:(NSArray<NSString*>*)sessionIDs
directory:(const base::FilePath&)directory
completion:(base::OnceClosure)callback {
NSMutableArray<NSString*>* paths =
[NSMutableArray arrayWithCapacity:sessionIDs.count];
for (NSString* sessionID : sessionIDs) {
[paths addObject:[SessionServiceIOS sessionPathForSessionID:sessionID
directory:directory]];
}
[self deletePaths:paths completion:std::move(callback)];
}
+ (NSString*)sessionPathForSessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory {
DCHECK(sessionID.length != 0);
return base::apple::FilePathToNSString(
directory.Append(kSessions)
.Append(base::SysNSStringToUTF8(sessionID))
.Append(kSessionFileName));
}
+ (NSString*)filePathForTabID:(NSString*)tabID
sessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory {
return [self filePathForTabID:tabID
sessionPath:[self sessionPathForSessionID:sessionID
directory:directory]];
}
+ (NSString*)filePathForTabID:(NSString*)tabID
sessionPath:(NSString*)sessionPath {
return [NSString stringWithFormat:@"%@-%@", sessionPath, tabID];
}
#pragma mark - Private methods
// Delete files/folders of the given `paths`.
- (void)deletePaths:(NSArray<NSString*>*)paths
completion:(base::OnceClosure)callback {
_taskRunner->PostTaskAndReply(
FROM_HERE, base::BindOnce(^{
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
NSFileManager* fileManager = [NSFileManager defaultManager];
for (NSString* path : paths) {
if (![fileManager fileExistsAtPath:path])
continue;
[self deleteSessionPaths:path];
}
}),
std::move(callback));
}
- (void)deleteSessionPaths:(NSString*)sessionPath {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* directory = [sessionPath stringByDeletingLastPathComponent];
NSString* sessionFilename = [sessionPath lastPathComponent];
NSError* error = nil;
BOOL isDirectory = NO;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] ||
!isDirectory) {
return;
}
NSArray<NSString*>* fileList =
[fileManager contentsOfDirectoryAtPath:directory error:&error];
if (error) {
CHECK(false) << "Unable to get session path list: "
<< base::SysNSStringToUTF8(directory) << ": "
<< base::SysNSStringToUTF8([error description]);
}
for (NSString* filename : fileList) {
if (![filename hasPrefix:sessionFilename]) {
continue;
}
NSString* filepath = [directory stringByAppendingPathComponent:filename];
if (![fileManager fileExistsAtPath:filepath isDirectory:&isDirectory] ||
isDirectory) {
continue;
}
if (![fileManager removeItemAtPath:filepath error:&error] || error) {
CHECK(false) << "Unable to delete path: "
<< base::SysNSStringToUTF8(filepath) << ": "
<< base::SysNSStringToUTF8([error description]);
}
}
}
// Do the work of saving on a background thread.
- (void)performSaveToPathInBackground:(NSString*)sessionPath {
DCHECK(sessionPath);
const base::TimeTicks start_time = base::TimeTicks::Now();
// Serialize to NSData on the main thread to avoid accessing potentially
// non-threadsafe objects on a background thread.
SessionIOSFactory* factory = [_pendingSessions objectForKey:sessionPath];
[_pendingSessions removeObjectForKey:sessionPath];
SessionIOS* session = [factory sessionForSaving];
// Because the factory may be called asynchronously after the underlying
// web state list is destroyed, the session may be nil; if so, do nothing.
// Do not record the time spent calling -sessionForSaving: as it not
// interesting in that case.
if (!session)
return;
@try {
NSError* error = nil;
size_t previous_cert_policy_bytes = web::GetCertPolicyBytesEncoded();
const base::TimeTicks archiving_start_time = base::TimeTicks::Now();
NSData* sessionData = [NSKeyedArchiver archivedDataWithRootObject:session
requiringSecureCoding:NO
error:&error];
// Store end_time to avoid counting the time spent recording the first
// metric as part of the second metric recorded (probably negligible).
const base::TimeTicks end_time = base::TimeTicks::Now();
base::UmaHistogramTimes("Session.WebStates.SavingTimeOnMainThread",
end_time - start_time);
base::UmaHistogramTimes("Session.WebStates.ArchivedDataWithRootObjectTime",
end_time - archiving_start_time);
if (!sessionData || error) {
DLOG(WARNING) << "Error serializing session for path: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([error description]);
return;
}
base::UmaHistogramCounts100000(
"Session.WebStates.AllSerializedCertPolicyCachesSize",
web::GetCertPolicyBytesEncoded() - previous_cert_policy_bytes / 1024);
base::UmaHistogramCounts100000("Session.WebStates.SerializedSize",
sessionData.length / 1024);
_taskRunner->PostTask(FROM_HERE, base::BindOnce(^{
[self performSaveSessionData:sessionData
sessionPath:sessionPath];
}));
} @catch (NSException* exception) {
NOTREACHED() << "Error serializing session for path: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([exception description]);
return;
}
}
@end
@implementation SessionServiceIOS (SubClassing)
- (void)performSaveSessionData:(NSData*)sessionData
sessionPath:(NSString*)sessionPath {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* directory = [sessionPath stringByDeletingLastPathComponent];
NSError* error = nil;
BOOL isDirectory = NO;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory]) {
isDirectory = YES;
if (![fileManager createDirectoryAtPath:directory
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
DLOG(WARNING) << "Error creating destination directory: "
<< base::SysNSStringToUTF8(directory) << ": "
<< base::SysNSStringToUTF8([error description]);
return;
}
}
if (!isDirectory) {
NOTREACHED() << "Error creating destination directory: "
<< base::SysNSStringToUTF8(directory) << ": "
<< "file exists and is not a directory.";
return;
}
NSDataWritingOptions options =
NSDataWritingAtomic |
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication;
base::TimeTicks start_time = base::TimeTicks::Now();
if (![sessionData writeToFile:sessionPath options:options error:&error]) {
DLOG(WARNING) << "Error writing session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([error description]);
return;
}
UmaHistogramTimes("Session.WebStates.WriteToFileTime",
base::TimeTicks::Now() - start_time);
}
@end