// Copyright (c) 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 "chrome/browser/sessions/session_backend.h" #include <limits> #include "base/file_util.h" #include "base/memory/scoped_vector.h" #include "base/metrics/histogram.h" #include "base/threading/thread_restrictions.h" #include "net/base/file_stream.h" #include "net/base/net_errors.h" using base::TimeTicks; // 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 SessionCommand::id_type id_type; typedef 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 net::FileStream(NULL)); if (base::PathExists(path)) file_->OpenSync(path, base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_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(BaseSessionService::SessionType type, std::vector<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. 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<net::FileStream> 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(BaseSessionService::SessionType type, std::vector<SessionCommand*>* commands) { if (!file_->IsOpen()) return false; FileHeader header; int read_count; TimeTicks start_time = TimeTicks::Now(); read_count = file_->ReadUntilComplete(reinterpret_cast<char*>(&header), sizeof(header)); if (read_count != sizeof(header) || header.signature != kFileSignature || header.version != kFileCurrentVersion) return false; ScopedVector<SessionCommand> read_commands; SessionCommand* command; while ((command = ReadCommand()) && !errored_) read_commands.push_back(command); if (!errored_) read_commands.swap(*commands); if (type == 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_; } 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. SessionCommand* command = new 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_->ReadUntilComplete(&(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(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( std::vector<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_->IsOpen()) { ResetFile(); } // Need to check current_session_file_ again, ResetFile may fail. if (current_session_file_.get() && current_session_file_->IsOpen() && !AppendCommandsToFile(current_session_file_.get(), *commands)) { current_session_file_.reset(NULL); } empty_file_ = false; STLDeleteElements(commands); delete commands; } void SessionBackend::ReadLastSessionCommands( const CancelableTaskTracker::IsCanceledCallback& is_canceled, const BaseSessionService::InternalGetCommandsCallback& callback) { if (is_canceled.Run()) return; Init(); ScopedVector<SessionCommand> commands; ReadLastSessionCommandsImpl(&(commands.get())); callback.Run(commands.Pass()); } bool SessionBackend::ReadLastSessionCommandsImpl( std::vector<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_ == 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( std::vector<SessionCommand*>* commands) { Init(); SessionFileReader file_reader(GetCurrentSessionPath()); return file_reader.Read(type_, commands); } bool SessionBackend::AppendCommandsToFile(net::FileStream* file, const std::vector<SessionCommand*>& commands) { for (std::vector<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_ == BaseSessionService::TAB_RESTORE) UMA_HISTOGRAM_COUNTS("TabRestore.command_size", total_size); else UMA_HISTOGRAM_COUNTS("SessionRestore.command_size", total_size); wrote = file->WriteSync(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->WriteSync(reinterpret_cast<char*>(&command_id), sizeof(command_id)); if (wrote != sizeof(command_id)) { NOTREACHED() << "error writing"; return false; } if (content_size > 0) { wrote = file->WriteSync(reinterpret_cast<char*>((*i)->contents()), content_size); if (wrote != content_size) { NOTREACHED() << "error writing"; return false; } } #if defined(OS_CHROMEOS) // TODO(gspencer): Remove this once we find a better place to do it. // See issue http://crbug.com/245015 file->FlushSync(); #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_->Truncate(header_size) != header_size) current_session_file_.reset(NULL); } if (!current_session_file_.get()) current_session_file_.reset(OpenAndWriteHeader(GetCurrentSessionPath())); empty_file_ = true; } net::FileStream* SessionBackend::OpenAndWriteHeader( const base::FilePath& path) { DCHECK(!path.empty()); scoped_ptr<net::FileStream> file(new net::FileStream(NULL)); if (file->OpenSync(path, base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE | base::PLATFORM_FILE_EXCLUSIVE_WRITE | base::PLATFORM_FILE_EXCLUSIVE_READ) != net::OK) return NULL; FileHeader header; header.signature = kFileSignature; header.version = kFileCurrentVersion; int wrote = file->WriteSync(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_ == 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_ == BaseSessionService::TAB_RESTORE) path = path.AppendASCII(kCurrentTabSessionFileName); else path = path.AppendASCII(kCurrentSessionFileName); return path; }