// 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/model/session_service_ios.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/check_op.h"
#import "base/files/file_path.h"
#import "base/format_macros.h"
#import "base/functional/bind.h"
#import "base/functional/callback.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 "base/timer/timer.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_internal_util.h"
#import "ios/chrome/browser/sessions/model/session_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios_factory.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"
#import "ios/web/public/web_state_id.h"
namespace {
// Callback invoked to request saving session at path using factory.
using SaveSessionCallback =
base::RepeatingCallback<void(NSString*, SessionWindowIOSFactory*)>;
} // namespace
// Represents a pending save request.
@interface SaveSessionRequest : NSObject
// Designated initializer.
- (instancetype)initWithPath:(NSString*)path
deadline:(base::TimeTicks)deadline
factory:(__weak SessionWindowIOSFactory*)factory
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
// Path at which the data needs to be saved on disk.
@property(nonatomic, readonly) NSString* path;
// Time at which the data needs to be saved on disk.
@property(nonatomic, readonly) base::TimeTicks deadline;
// Factory used to generate the data to save to disk.
@property(nonatomic, weak, readonly) SessionWindowIOSFactory* factory;
@end
@implementation SaveSessionRequest
- (instancetype)initWithPath:(NSString*)path
deadline:(base::TimeTicks)deadline
factory:(__weak SessionWindowIOSFactory*)factory {
if ((self = [super init])) {
DCHECK(path.length);
_path = [path copy];
_deadline = deadline;
_factory = factory;
}
return self;
}
@end
// Represents a queue of pending save requests.
@interface SaveSessionRequestQueue : NSObject
// Designated initializer.
- (instancetype)initWithCallback:(SaveSessionCallback)callback
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
// Called before destroying the task runner.
- (void)shutdown;
// Schedules a new requests to save data at `path` after `delay` using
// `factory`. Ignored if a request scheduled for `path` with a closer
// deadline is already scheduled.
- (void)scheduleRequestForPath:(NSString*)path
delay:(base::TimeDelta)delay
factory:(__weak SessionWindowIOSFactory*)factory;
@end
@implementation SaveSessionRequestQueue {
// Priority queue storing the pending requests ordered by deadline.
std::multimap<base::TimeTicks, SaveSessionRequest*> _priority;
// Dictionary mapping session path to the pending save request for that path.
NSMutableDictionary<NSString*, SaveSessionRequest*>* _pending;
// Callback passed to the constructor and invoked to save a session when
// the deadline for a request expires.
SaveSessionCallback _callback;
// Timer used to wait until the next pending request deadline expires.
base::OneShotTimer _timer;
}
- (instancetype)initWithCallback:(SaveSessionCallback)callback {
if ((self = [super init])) {
_pending = [[NSMutableDictionary alloc] init];
_callback = std::move(callback);
DCHECK(!_callback.is_null());
}
return self;
}
- (void)shutdown {
_callback = base::NullCallback();
_timer.Stop();
}
- (void)scheduleRequestForPath:(NSString*)path
delay:(base::TimeDelta)delay
factory:(__weak SessionWindowIOSFactory*)factory {
DCHECK(path.length);
DCHECK_GE(delay, base::TimeDelta()); // Can't schedule in the past.
const base::TimeTicks deadline = base::TimeTicks::Now() + delay;
SaveSessionRequest* request = [_pending objectForKey:path];
if (request) {
// The existing request is scheduled with a shorter deadline, ignore the
// new request. Return early as there is nothing to do.
if (request.deadline <= deadline) {
return;
}
// Drop the old request as the new one will expire sooner.
auto range = _priority.equal_range(request.deadline);
for (auto iter = range.first; iter != range.second; ++iter) {
if (iter->second == request) {
_priority.erase(iter);
break;
}
}
[_pending removeObjectForKey:path];
}
request = [[SaveSessionRequest alloc] initWithPath:path
deadline:deadline
factory:factory];
// Need to reset the timer if the newly scheduled request will have the
// closest deadline.
const bool resetTimer =
_priority.empty() || deadline < _priority.begin()->first;
[_pending setObject:request forKey:path];
_priority.insert(std::make_pair(deadline, request));
if (resetTimer) {
[self resetTimerWithDelay:delay];
}
}
// Resets the timer to expire in `delay`. If the delay is zero, then consider
// the timer expires immediately and instead call the timer expiration method.
- (void)resetTimerWithDelay:(base::TimeDelta)delay {
DCHECK(!_priority.empty());
DCHECK_GE(delay, base::TimeDelta());
if (delay == base::TimeDelta()) {
// No delay, stop the timer and consider it as immediately expired.
_timer.Stop();
[self onTimerExpired];
return;
}
__weak SaveSessionRequestQueue* weakSelf = self;
_timer.Start(FROM_HERE, delay, base::BindOnce(^{
[weakSelf onTimerExpired];
}));
}
// Invoked when the timer expires. Should only happens when the priority queue
// is not empty, and at least one item is scheduled to expire now or in the
// past.
- (void)onTimerExpired {
const base::TimeTicks now = base::TimeTicks::Now();
while (!_priority.empty()) {
auto iter = _priority.begin();
if (iter->first > now) {
[self resetTimerWithDelay:(iter->first - now)];
break;
}
SaveSessionRequest* request = iter->second;
[_pending removeObjectForKey:request.path];
_priority.erase(iter);
_callback.Run(request.path, request.factory);
}
}
@end
@implementation SessionServiceIOS {
// The SequencedTaskRunner on which File IO operations are performed.
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
// Delay before saving data to storage when not saving session immediately.
base::TimeDelta _saveDelay;
// Queue of pending save requests.
SaveSessionRequestQueue* _pendingRequests;
}
#pragma mark - Public interface
- (instancetype)initWithSaveDelay:(base::TimeDelta)saveDelay
taskRunner:
(const scoped_refptr<base::SequencedTaskRunner>&)
taskRunner {
DCHECK(taskRunner);
DCHECK_GT(saveDelay, base::Seconds(0));
self = [super init];
if (self) {
_taskRunner = taskRunner;
_saveDelay = saveDelay;
__weak SessionServiceIOS* weakSelf = self;
auto savingBlock = ^(NSString* path, SessionWindowIOSFactory* factory) {
[weakSelf saveSessionToPath:path usingFactory:factory];
};
_pendingRequests = [[SaveSessionRequestQueue alloc]
initWithCallback:base::BindRepeating(savingBlock)];
}
return self;
}
- (void)shutdown {
[_pendingRequests shutdown];
_pendingRequests = nil;
_taskRunner.reset();
}
- (void)shutdownWithClosure:(base::OnceClosure)closure {
_taskRunner->PostTask(FROM_HERE, std::move(closure));
}
- (void)saveSession:(__weak SessionWindowIOSFactory*)factory
sessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory
immediately:(BOOL)immediately {
NSString* sessionPath = [[self class] sessionPathForSessionID:sessionID
directory:directory];
const base::TimeDelta delay = immediately ? base::TimeDelta() : _saveDelay;
[_pendingRequests scheduleRequestForPath:sessionPath
delay:delay
factory:factory];
}
- (SessionWindowIOS*)loadSessionWithSessionID:(NSString*)sessionID
directory:(const base::FilePath&)directory {
NSString* sessionPath = [[self class] sessionPathForSessionID:sessionID
directory:directory];
base::TimeTicks start_time = base::TimeTicks::Now();
SessionWindowIOS* session = [self loadSessionFromPath:sessionPath];
UmaHistogramTimes("Session.WebStates.ReadFromFileTime",
base::TimeTicks::Now() - start_time);
return session;
}
- (SessionWindowIOS*)loadSessionFromPath:(NSString*)sessionPath {
SessionWindowIOS* sessionWindowIOS = ios::sessions::ReadSessionWindow(
base::apple::NSStringToFilePath(sessionPath));
// If the identifiers loaded from disk are invalid, assign new identifiers.
for (CRWSessionStorage* sessionStorage in sessionWindowIOS.sessions) {
if (!sessionStorage.uniqueIdentifier.valid()) {
sessionStorage.uniqueIdentifier = web::WebStateID::NewUnique();
}
}
return sessionWindowIOS;
}
- (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(kLegacySessionsDirname)
.Append(base::SysNSStringToUTF8(sessionID))
.Append(kLegacySessionFilename));
}
+ (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)saveSessionToPath:(NSString*)sessionPath
usingFactory:(SessionWindowIOSFactory*)factory {
DCHECK(sessionPath);
// Serialize to NSData on the main thread to avoid accessing potentially
// non-threadsafe objects on a background thread.
const base::TimeTicks start_time = base::TimeTicks::Now();
SessionWindowIOS* sessionWindow = [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 (!sessionWindow) {
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:sessionWindow
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(kSessionHistogramSavingTime, 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_IN_MIGRATION()
<< "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_IN_MIGRATION() << "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