1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/extensions/activity_log/activity_database.h"
6
7#include <string>
8
9#include "base/command_line.h"
10#include "base/logging.h"
11#include "base/strings/string_util.h"
12#include "base/strings/stringprintf.h"
13#include "base/threading/thread.h"
14#include "base/threading/thread_checker.h"
15#include "base/time/clock.h"
16#include "base/time/time.h"
17#include "chrome/browser/extensions/activity_log/fullstream_ui_policy.h"
18#include "chrome/common/chrome_switches.h"
19#include "sql/error_delegate_util.h"
20#include "sql/transaction.h"
21#include "third_party/sqlite/sqlite3.h"
22
23#if defined(OS_MACOSX)
24#include "base/mac/mac_util.h"
25#endif
26
27using content::BrowserThread;
28
29namespace extensions {
30
31// A size threshold at which data should be flushed to the database.  The
32// ActivityDatabase will signal the Delegate to write out data based on a
33// periodic timer, but will also initiate a flush if AdviseFlush indicates that
34// more than kSizeThresholdForFlush action records are queued in memory.  This
35// should be set large enough that write costs can be amortized across many
36// records, but not so large that too much space can be tied up holding records
37// in memory.
38static const int kSizeThresholdForFlush = 200;
39
40ActivityDatabase::ActivityDatabase(ActivityDatabase::Delegate* delegate)
41    : delegate_(delegate),
42      valid_db_(false),
43      batch_mode_(true),
44      already_closed_(false),
45      did_init_(false) {
46  if (CommandLine::ForCurrentProcess()->HasSwitch(
47          switches::kEnableExtensionActivityLogTesting)) {
48    batching_period_ = base::TimeDelta::FromSeconds(10);
49  } else {
50    batching_period_ = base::TimeDelta::FromMinutes(2);
51  }
52}
53
54ActivityDatabase::~ActivityDatabase() {}
55
56void ActivityDatabase::Init(const base::FilePath& db_name) {
57  if (did_init_) return;
58  did_init_ = true;
59  if (BrowserThread::IsMessageLoopValid(BrowserThread::DB))
60    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
61  db_.set_histogram_tag("Activity");
62  db_.set_error_callback(
63      base::Bind(&ActivityDatabase::DatabaseErrorCallback,
64                 base::Unretained(this)));
65  db_.set_page_size(4096);
66  db_.set_cache_size(32);
67
68  if (!db_.Open(db_name)) {
69    LOG(ERROR) << db_.GetErrorMessage();
70    return LogInitFailure();
71  }
72
73  // Wrap the initialization in a transaction so that the db doesn't
74  // get corrupted if init fails/crashes.
75  sql::Transaction committer(&db_);
76  if (!committer.Begin())
77    return LogInitFailure();
78
79#if defined(OS_MACOSX)
80  // Exclude the database from backups.
81  base::mac::SetFileBackupExclusion(db_name);
82#endif
83
84  if (!delegate_->InitDatabase(&db_))
85    return LogInitFailure();
86
87  sql::InitStatus stat = committer.Commit() ? sql::INIT_OK : sql::INIT_FAILURE;
88  if (stat != sql::INIT_OK)
89    return LogInitFailure();
90
91  // Pre-loads the first <cache-size> pages into the cache.
92  // Doesn't do anything if the database is new.
93  db_.Preload();
94
95  valid_db_ = true;
96  timer_.Start(FROM_HERE,
97               batching_period_,
98               this,
99               &ActivityDatabase::RecordBatchedActions);
100}
101
102void ActivityDatabase::LogInitFailure() {
103  LOG(ERROR) << "Couldn't initialize the activity log database.";
104  SoftFailureClose();
105}
106
107void ActivityDatabase::AdviseFlush(int size) {
108  if (!valid_db_)
109    return;
110  if (!batch_mode_ || size == kFlushImmediately ||
111      size >= kSizeThresholdForFlush) {
112    if (!delegate_->FlushDatabase(&db_))
113      SoftFailureClose();
114  }
115}
116
117void ActivityDatabase::RecordBatchedActions() {
118  if (valid_db_) {
119    if (!delegate_->FlushDatabase(&db_))
120      SoftFailureClose();
121  }
122}
123
124void ActivityDatabase::SetBatchModeForTesting(bool batch_mode) {
125  if (batch_mode && !batch_mode_) {
126    timer_.Start(FROM_HERE,
127                 batching_period_,
128                 this,
129                 &ActivityDatabase::RecordBatchedActions);
130  } else if (!batch_mode && batch_mode_) {
131    timer_.Stop();
132    RecordBatchedActions();
133  }
134  batch_mode_ = batch_mode;
135}
136
137sql::Connection* ActivityDatabase::GetSqlConnection() {
138  if (BrowserThread::IsMessageLoopValid(BrowserThread::DB))
139    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
140  if (valid_db_) {
141    return &db_;
142  } else {
143    LOG(WARNING) << "Activity log database is not valid";
144    return NULL;
145  }
146}
147
148void ActivityDatabase::Close() {
149  timer_.Stop();
150  if (!already_closed_) {
151    RecordBatchedActions();
152    db_.reset_error_callback();
153  }
154  valid_db_ = false;
155  already_closed_ = true;
156  // Call DatabaseCloseCallback() just before deleting the ActivityDatabase
157  // itself--these two objects should have the same lifetime.
158  delegate_->OnDatabaseClose();
159  delete this;
160}
161
162void ActivityDatabase::HardFailureClose() {
163  if (already_closed_) return;
164  valid_db_ = false;
165  timer_.Stop();
166  db_.reset_error_callback();
167  db_.RazeAndClose();
168  delegate_->OnDatabaseFailure();
169  already_closed_ = true;
170}
171
172void ActivityDatabase::SoftFailureClose() {
173  valid_db_ = false;
174  timer_.Stop();
175  delegate_->OnDatabaseFailure();
176}
177
178void ActivityDatabase::DatabaseErrorCallback(int error, sql::Statement* stmt) {
179  if (sql::IsErrorCatastrophic(error)) {
180    LOG(ERROR) << "Killing the ActivityDatabase due to catastrophic error.";
181    HardFailureClose();
182  } else if (error != SQLITE_BUSY) {
183    // We ignore SQLITE_BUSY errors because they are presumably transient.
184    LOG(ERROR) << "Closing the ActivityDatabase due to error.";
185    SoftFailureClose();
186  }
187}
188
189void ActivityDatabase::RecordBatchedActionsWhileTesting() {
190  RecordBatchedActions();
191  timer_.Stop();
192}
193
194void ActivityDatabase::SetTimerForTesting(int ms) {
195  timer_.Stop();
196  timer_.Start(FROM_HERE,
197               base::TimeDelta::FromMilliseconds(ms),
198               this,
199               &ActivityDatabase::RecordBatchedActionsWhileTesting);
200}
201
202// static
203bool ActivityDatabase::InitializeTable(sql::Connection* db,
204                                       const char* table_name,
205                                       const char* content_fields[],
206                                       const char* field_types[],
207                                       const int num_content_fields) {
208  if (!db->DoesTableExist(table_name)) {
209    std::string table_creator =
210        base::StringPrintf("CREATE TABLE %s (", table_name);
211    for (int i = 0; i < num_content_fields; i++) {
212      table_creator += base::StringPrintf("%s%s %s",
213                                          i == 0 ? "" : ", ",
214                                          content_fields[i],
215                                          field_types[i]);
216    }
217    table_creator += ")";
218    if (!db->Execute(table_creator.c_str()))
219      return false;
220  } else {
221    // In case we ever want to add new fields, this initializes them to be
222    // empty strings.
223    for (int i = 0; i < num_content_fields; i++) {
224      if (!db->DoesColumnExist(table_name, content_fields[i])) {
225        std::string table_updater = base::StringPrintf(
226            "ALTER TABLE %s ADD COLUMN %s %s; ",
227             table_name,
228             content_fields[i],
229             field_types[i]);
230        if (!db->Execute(table_updater.c_str()))
231          return false;
232      }
233    }
234  }
235  return true;
236}
237
238}  // namespace extensions
239