| // Copyright 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/sessions/session_backend.h" |
| |
| #include <limits> |
| |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/scoped_vector.h" |
| #include "base/metrics/histogram.h" |
| #include "base/threading/thread_restrictions.h" |
| |
| using base::TimeTicks; |
| |
| namespace sessions { |
| |
| // File version number. |
| static const int32 kFileCurrentVersion = 1; |
| |
| // The signature at the beginning of the file = SSNS (Sessions). |
| static const int32 kFileSignature = 0x53534E53; |
| |
| namespace { |
| |
| // The file header is the first bytes written to the file, |
| // and is used to identify the file as one written by us. |
| struct FileHeader { |
| int32 signature; |
| int32 version; |
| }; |
| |
| // SessionFileReader ---------------------------------------------------------- |
| |
| // SessionFileReader is responsible for reading the set of SessionCommands that |
| // describe a Session back from a file. SessionFileRead does minimal error |
| // checking on the file (pretty much only that the header is valid). |
| |
| class SessionFileReader { |
| public: |
| typedef sessions::SessionCommand::id_type id_type; |
| typedef sessions::SessionCommand::size_type size_type; |
| |
| explicit SessionFileReader(const base::FilePath& path) |
| : errored_(false), |
| buffer_(SessionBackend::kFileReadBufferSize, 0), |
| buffer_position_(0), |
| available_count_(0) { |
| file_.reset(new base::File( |
| path, base::File::FLAG_OPEN | base::File::FLAG_READ)); |
| } |
| // Reads the contents of the file specified in the constructor, returning |
| // true on success. It is up to the caller to free all SessionCommands |
| // added to commands. |
| bool Read(sessions::BaseSessionService::SessionType type, |
| ScopedVector<sessions::SessionCommand>* commands); |
| |
| private: |
| // Reads a single command, returning it. A return value of NULL indicates |
| // either there are no commands, or there was an error. Use errored_ to |
| // distinguish the two. If NULL is returned, and there is no error, it means |
| // the end of file was successfully reached. |
| sessions::SessionCommand* ReadCommand(); |
| |
| // Shifts the unused portion of buffer_ to the beginning and fills the |
| // remaining portion with data from the file. Returns false if the buffer |
| // couldn't be filled. A return value of false only signals an error if |
| // errored_ is set to true. |
| bool FillBuffer(); |
| |
| // Whether an error condition has been detected ( |
| bool errored_; |
| |
| // As we read from the file, data goes here. |
| std::string buffer_; |
| |
| // The file. |
| scoped_ptr<base::File> file_; |
| |
| // Position in buffer_ of the data. |
| size_t buffer_position_; |
| |
| // Number of available bytes; relative to buffer_position_. |
| size_t available_count_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SessionFileReader); |
| }; |
| |
| bool SessionFileReader::Read(sessions::BaseSessionService::SessionType type, |
| ScopedVector<sessions::SessionCommand>* commands) { |
| if (!file_->IsValid()) |
| return false; |
| FileHeader header; |
| int read_count; |
| TimeTicks start_time = TimeTicks::Now(); |
| read_count = file_->ReadAtCurrentPos(reinterpret_cast<char*>(&header), |
| sizeof(header)); |
| if (read_count != sizeof(header) || header.signature != kFileSignature || |
| header.version != kFileCurrentVersion) |
| return false; |
| |
| ScopedVector<sessions::SessionCommand> read_commands; |
| for (sessions::SessionCommand* command = ReadCommand(); command && !errored_; |
| command = ReadCommand()) |
| read_commands.push_back(command); |
| if (!errored_) |
| read_commands.swap(*commands); |
| if (type == sessions::BaseSessionService::TAB_RESTORE) { |
| UMA_HISTOGRAM_TIMES("TabRestore.read_session_file_time", |
| TimeTicks::Now() - start_time); |
| } else { |
| UMA_HISTOGRAM_TIMES("SessionRestore.read_session_file_time", |
| TimeTicks::Now() - start_time); |
| } |
| return !errored_; |
| } |
| |
| sessions::SessionCommand* SessionFileReader::ReadCommand() { |
| // Make sure there is enough in the buffer for the size of the next command. |
| if (available_count_ < sizeof(size_type)) { |
| if (!FillBuffer()) |
| return NULL; |
| if (available_count_ < sizeof(size_type)) { |
| VLOG(1) << "SessionFileReader::ReadCommand, file incomplete"; |
| // Still couldn't read a valid size for the command, assume write was |
| // incomplete and return NULL. |
| return NULL; |
| } |
| } |
| // Get the size of the command. |
| size_type command_size; |
| memcpy(&command_size, &(buffer_[buffer_position_]), sizeof(command_size)); |
| buffer_position_ += sizeof(command_size); |
| available_count_ -= sizeof(command_size); |
| |
| if (command_size == 0) { |
| VLOG(1) << "SessionFileReader::ReadCommand, empty command"; |
| // Empty command. Shouldn't happen if write was successful, fail. |
| return NULL; |
| } |
| |
| // Make sure buffer has the complete contents of the command. |
| if (command_size > available_count_) { |
| if (command_size > buffer_.size()) |
| buffer_.resize((command_size / 1024 + 1) * 1024, 0); |
| if (!FillBuffer() || command_size > available_count_) { |
| // Again, assume the file was ok, and just the last chunk was lost. |
| VLOG(1) << "SessionFileReader::ReadCommand, last chunk lost"; |
| return NULL; |
| } |
| } |
| const id_type command_id = buffer_[buffer_position_]; |
| // NOTE: command_size includes the size of the id, which is not part of |
| // the contents of the SessionCommand. |
| sessions::SessionCommand* command = |
| new sessions::SessionCommand(command_id, command_size - sizeof(id_type)); |
| if (command_size > sizeof(id_type)) { |
| memcpy(command->contents(), |
| &(buffer_[buffer_position_ + sizeof(id_type)]), |
| command_size - sizeof(id_type)); |
| } |
| buffer_position_ += command_size; |
| available_count_ -= command_size; |
| return command; |
| } |
| |
| bool SessionFileReader::FillBuffer() { |
| if (available_count_ > 0 && buffer_position_ > 0) { |
| // Shift buffer to beginning. |
| memmove(&(buffer_[0]), &(buffer_[buffer_position_]), available_count_); |
| } |
| buffer_position_ = 0; |
| DCHECK(buffer_position_ + available_count_ < buffer_.size()); |
| int to_read = static_cast<int>(buffer_.size() - available_count_); |
| int read_count = file_->ReadAtCurrentPos(&(buffer_[available_count_]), |
| to_read); |
| if (read_count < 0) { |
| errored_ = true; |
| return false; |
| } |
| if (read_count == 0) |
| return false; |
| available_count_ += read_count; |
| return true; |
| } |
| |
| } // namespace |
| |
| // SessionBackend ------------------------------------------------------------- |
| |
| // File names (current and previous) for a type of TAB. |
| static const char* kCurrentTabSessionFileName = "Current Tabs"; |
| static const char* kLastTabSessionFileName = "Last Tabs"; |
| |
| // File names (current and previous) for a type of SESSION. |
| static const char* kCurrentSessionFileName = "Current Session"; |
| static const char* kLastSessionFileName = "Last Session"; |
| |
| // static |
| const int SessionBackend::kFileReadBufferSize = 1024; |
| |
| SessionBackend::SessionBackend(sessions::BaseSessionService::SessionType type, |
| const base::FilePath& path_to_dir) |
| : type_(type), |
| path_to_dir_(path_to_dir), |
| last_session_valid_(false), |
| inited_(false), |
| empty_file_(true) { |
| // NOTE: this is invoked on the main thread, don't do file access here. |
| } |
| |
| void SessionBackend::Init() { |
| if (inited_) |
| return; |
| |
| inited_ = true; |
| |
| // Create the directory for session info. |
| base::CreateDirectory(path_to_dir_); |
| |
| MoveCurrentSessionToLastSession(); |
| } |
| |
| void SessionBackend::AppendCommands( |
| ScopedVector<sessions::SessionCommand> commands, |
| bool reset_first) { |
| Init(); |
| // Make sure and check current_session_file_, if opening the file failed |
| // current_session_file_ will be NULL. |
| if ((reset_first && !empty_file_) || !current_session_file_.get() || |
| !current_session_file_->IsValid()) { |
| ResetFile(); |
| } |
| // Need to check current_session_file_ again, ResetFile may fail. |
| if (current_session_file_.get() && current_session_file_->IsValid() && |
| !AppendCommandsToFile(current_session_file_.get(), commands)) { |
| current_session_file_.reset(NULL); |
| } |
| empty_file_ = false; |
| } |
| |
| void SessionBackend::ReadLastSessionCommands( |
| const base::CancelableTaskTracker::IsCanceledCallback& is_canceled, |
| const sessions::BaseSessionService::GetCommandsCallback& callback) { |
| if (is_canceled.Run()) |
| return; |
| |
| Init(); |
| |
| ScopedVector<sessions::SessionCommand> commands; |
| ReadLastSessionCommandsImpl(&commands); |
| callback.Run(commands.Pass()); |
| } |
| |
| bool SessionBackend::ReadLastSessionCommandsImpl( |
| ScopedVector<sessions::SessionCommand>* commands) { |
| Init(); |
| SessionFileReader file_reader(GetLastSessionPath()); |
| return file_reader.Read(type_, commands); |
| } |
| |
| void SessionBackend::DeleteLastSession() { |
| Init(); |
| base::DeleteFile(GetLastSessionPath(), false); |
| } |
| |
| void SessionBackend::MoveCurrentSessionToLastSession() { |
| Init(); |
| current_session_file_.reset(NULL); |
| |
| const base::FilePath current_session_path = GetCurrentSessionPath(); |
| const base::FilePath last_session_path = GetLastSessionPath(); |
| if (base::PathExists(last_session_path)) |
| base::DeleteFile(last_session_path, false); |
| if (base::PathExists(current_session_path)) { |
| int64 file_size; |
| if (base::GetFileSize(current_session_path, &file_size)) { |
| if (type_ == sessions::BaseSessionService::TAB_RESTORE) { |
| UMA_HISTOGRAM_COUNTS("TabRestore.last_session_file_size", |
| static_cast<int>(file_size / 1024)); |
| } else { |
| UMA_HISTOGRAM_COUNTS("SessionRestore.last_session_file_size", |
| static_cast<int>(file_size / 1024)); |
| } |
| } |
| last_session_valid_ = base::Move(current_session_path, last_session_path); |
| } |
| |
| if (base::PathExists(current_session_path)) |
| base::DeleteFile(current_session_path, false); |
| |
| // Create and open the file for the current session. |
| ResetFile(); |
| } |
| |
| bool SessionBackend::ReadCurrentSessionCommandsImpl( |
| ScopedVector<sessions::SessionCommand>* commands) { |
| Init(); |
| SessionFileReader file_reader(GetCurrentSessionPath()); |
| return file_reader.Read(type_, commands); |
| } |
| |
| bool SessionBackend::AppendCommandsToFile(base::File* file, |
| const ScopedVector<sessions::SessionCommand>& commands) { |
| for (ScopedVector<sessions::SessionCommand>::const_iterator i = |
| commands.begin(); |
| i != commands.end(); ++i) { |
| int wrote; |
| const size_type content_size = static_cast<size_type>((*i)->size()); |
| const size_type total_size = content_size + sizeof(id_type); |
| if (type_ == sessions::BaseSessionService::TAB_RESTORE) |
| UMA_HISTOGRAM_COUNTS("TabRestore.command_size", total_size); |
| else |
| UMA_HISTOGRAM_COUNTS("SessionRestore.command_size", total_size); |
| wrote = file->WriteAtCurrentPos(reinterpret_cast<const char*>(&total_size), |
| sizeof(total_size)); |
| if (wrote != sizeof(total_size)) { |
| NOTREACHED() << "error writing"; |
| return false; |
| } |
| id_type command_id = (*i)->id(); |
| wrote = file->WriteAtCurrentPos(reinterpret_cast<char*>(&command_id), |
| sizeof(command_id)); |
| if (wrote != sizeof(command_id)) { |
| NOTREACHED() << "error writing"; |
| return false; |
| } |
| if (content_size > 0) { |
| wrote = file->WriteAtCurrentPos(reinterpret_cast<char*>((*i)->contents()), |
| content_size); |
| if (wrote != content_size) { |
| NOTREACHED() << "error writing"; |
| return false; |
| } |
| } |
| } |
| #if defined(OS_CHROMEOS) |
| file->Flush(); |
| #endif |
| return true; |
| } |
| |
| SessionBackend::~SessionBackend() { |
| if (current_session_file_.get()) { |
| // Destructor performs file IO because file is open in sync mode. |
| // crbug.com/112512. |
| base::ThreadRestrictions::ScopedAllowIO allow_io; |
| current_session_file_.reset(); |
| } |
| } |
| |
| void SessionBackend::ResetFile() { |
| DCHECK(inited_); |
| if (current_session_file_.get()) { |
| // File is already open, truncate it. We truncate instead of closing and |
| // reopening to avoid the possibility of scanners locking the file out |
| // from under us once we close it. If truncation fails, we'll try to |
| // recreate. |
| const int header_size = static_cast<int>(sizeof(FileHeader)); |
| if (current_session_file_->Seek( |
| base::File::FROM_BEGIN, header_size) != header_size || |
| !current_session_file_->SetLength(header_size)) |
| current_session_file_.reset(NULL); |
| } |
| if (!current_session_file_.get()) |
| current_session_file_.reset(OpenAndWriteHeader(GetCurrentSessionPath())); |
| empty_file_ = true; |
| } |
| |
| base::File* SessionBackend::OpenAndWriteHeader(const base::FilePath& path) { |
| DCHECK(!path.empty()); |
| scoped_ptr<base::File> file(new base::File( |
| path, |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | |
| base::File::FLAG_EXCLUSIVE_WRITE | base::File::FLAG_EXCLUSIVE_READ)); |
| if (!file->IsValid()) |
| return NULL; |
| FileHeader header; |
| header.signature = kFileSignature; |
| header.version = kFileCurrentVersion; |
| int wrote = file->WriteAtCurrentPos(reinterpret_cast<char*>(&header), |
| sizeof(header)); |
| if (wrote != sizeof(header)) |
| return NULL; |
| return file.release(); |
| } |
| |
| base::FilePath SessionBackend::GetLastSessionPath() { |
| base::FilePath path = path_to_dir_; |
| if (type_ == sessions::BaseSessionService::TAB_RESTORE) |
| path = path.AppendASCII(kLastTabSessionFileName); |
| else |
| path = path.AppendASCII(kLastSessionFileName); |
| return path; |
| } |
| |
| base::FilePath SessionBackend::GetCurrentSessionPath() { |
| base::FilePath path = path_to_dir_; |
| if (type_ == sessions::BaseSessionService::TAB_RESTORE) |
| path = path.AppendASCII(kCurrentTabSessionFileName); |
| else |
| path = path.AppendASCII(kCurrentSessionFileName); |
| return path; |
| } |
| |
| } // namespace sessions |