1/*
2 *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#include "webrtc/base/filerotatingstream.h"
12
13#include <algorithm>
14#include <iostream>
15#include <string>
16
17#include "webrtc/base/checks.h"
18#include "webrtc/base/fileutils.h"
19#include "webrtc/base/pathutils.h"
20
21// Note: We use std::cerr for logging in the write paths of this stream to avoid
22// infinite loops when logging.
23
24namespace rtc {
25
26FileRotatingStream::FileRotatingStream(const std::string& dir_path,
27                                       const std::string& file_prefix)
28    : FileRotatingStream(dir_path, file_prefix, 0, 0, kRead) {
29}
30
31FileRotatingStream::FileRotatingStream(const std::string& dir_path,
32                                       const std::string& file_prefix,
33                                       size_t max_file_size,
34                                       size_t num_files)
35    : FileRotatingStream(dir_path,
36                         file_prefix,
37                         max_file_size,
38                         num_files,
39                         kWrite) {
40  RTC_DCHECK_GT(max_file_size, 0u);
41  RTC_DCHECK_GT(num_files, 1u);
42}
43
44FileRotatingStream::FileRotatingStream(const std::string& dir_path,
45                                       const std::string& file_prefix,
46                                       size_t max_file_size,
47                                       size_t num_files,
48                                       Mode mode)
49    : dir_path_(dir_path),
50      file_prefix_(file_prefix),
51      mode_(mode),
52      file_stream_(nullptr),
53      max_file_size_(max_file_size),
54      current_file_index_(0),
55      rotation_index_(0),
56      current_bytes_written_(0),
57      disable_buffering_(false) {
58  RTC_DCHECK(Filesystem::IsFolder(dir_path));
59  switch (mode) {
60    case kWrite: {
61      file_names_.clear();
62      for (size_t i = 0; i < num_files; ++i) {
63        file_names_.push_back(GetFilePath(i, num_files));
64      }
65      rotation_index_ = num_files - 1;
66      break;
67    }
68    case kRead: {
69      file_names_ = GetFilesWithPrefix();
70      std::sort(file_names_.begin(), file_names_.end());
71      if (file_names_.size() > 0) {
72        // |file_names_| is sorted newest first, so read from the end.
73        current_file_index_ = file_names_.size() - 1;
74      }
75      break;
76    }
77  }
78}
79
80FileRotatingStream::~FileRotatingStream() {
81}
82
83StreamState FileRotatingStream::GetState() const {
84  if (mode_ == kRead && current_file_index_ < file_names_.size()) {
85    return SS_OPEN;
86  }
87  if (!file_stream_) {
88    return SS_CLOSED;
89  }
90  return file_stream_->GetState();
91}
92
93StreamResult FileRotatingStream::Read(void* buffer,
94                                      size_t buffer_len,
95                                      size_t* read,
96                                      int* error) {
97  RTC_DCHECK(buffer);
98  if (mode_ != kRead) {
99    return SR_EOS;
100  }
101  if (current_file_index_ >= file_names_.size()) {
102    return SR_EOS;
103  }
104  // We will have no file stream initially, and when we are finished with the
105  // previous file.
106  if (!file_stream_) {
107    if (!OpenCurrentFile()) {
108      return SR_ERROR;
109    }
110  }
111  int local_error = 0;
112  if (!error) {
113    error = &local_error;
114  }
115  StreamResult result = file_stream_->Read(buffer, buffer_len, read, error);
116  if (result == SR_EOS || result == SR_ERROR) {
117    if (result == SR_ERROR) {
118      LOG(LS_ERROR) << "Failed to read from: "
119                    << file_names_[current_file_index_] << "Error: " << error;
120    }
121    // Reached the end of the file, read next file. If there is an error return
122    // the error status but allow for a next read by reading next file.
123    CloseCurrentFile();
124    if (current_file_index_ == 0) {
125      // Just finished reading the last file, signal EOS by setting index.
126      current_file_index_ = file_names_.size();
127    } else {
128      --current_file_index_;
129    }
130    if (read) {
131      *read = 0;
132    }
133    return result == SR_EOS ? SR_SUCCESS : result;
134  } else if (result == SR_SUCCESS) {
135    // Succeeded, continue reading from this file.
136    return SR_SUCCESS;
137  } else {
138    RTC_NOTREACHED();
139  }
140  return result;
141}
142
143StreamResult FileRotatingStream::Write(const void* data,
144                                       size_t data_len,
145                                       size_t* written,
146                                       int* error) {
147  if (mode_ != kWrite) {
148    return SR_EOS;
149  }
150  if (!file_stream_) {
151    std::cerr << "Open() must be called before Write." << std::endl;
152    return SR_ERROR;
153  }
154  // Write as much as will fit in to the current file.
155  RTC_DCHECK_LT(current_bytes_written_, max_file_size_);
156  size_t remaining_bytes = max_file_size_ - current_bytes_written_;
157  size_t write_length = std::min(data_len, remaining_bytes);
158  size_t local_written = 0;
159  if (!written) {
160    written = &local_written;
161  }
162  StreamResult result = file_stream_->Write(data, write_length, written, error);
163  current_bytes_written_ += *written;
164
165  // If we're done with this file, rotate it out.
166  if (current_bytes_written_ >= max_file_size_) {
167    RTC_DCHECK_EQ(current_bytes_written_, max_file_size_);
168    RotateFiles();
169  }
170  return result;
171}
172
173bool FileRotatingStream::Flush() {
174  if (!file_stream_) {
175    return false;
176  }
177  return file_stream_->Flush();
178}
179
180bool FileRotatingStream::GetSize(size_t* size) const {
181  if (mode_ != kRead) {
182    // Not possible to get accurate size on disk when writing because of
183    // potential buffering.
184    return false;
185  }
186  RTC_DCHECK(size);
187  *size = 0;
188  size_t total_size = 0;
189  for (auto file_name : file_names_) {
190    Pathname pathname(file_name);
191    size_t file_size = 0;
192    if (Filesystem::GetFileSize(file_name, &file_size)) {
193      total_size += file_size;
194    }
195  }
196  *size = total_size;
197  return true;
198}
199
200void FileRotatingStream::Close() {
201  CloseCurrentFile();
202}
203
204bool FileRotatingStream::Open() {
205  switch (mode_) {
206    case kRead:
207      // Defer opening to when we first read since we want to return read error
208      // if we fail to open next file.
209      return true;
210    case kWrite: {
211      // Delete existing files when opening for write.
212      std::vector<std::string> matching_files = GetFilesWithPrefix();
213      for (auto matching_file : matching_files) {
214        if (!Filesystem::DeleteFile(matching_file)) {
215          std::cerr << "Failed to delete: " << matching_file << std::endl;
216        }
217      }
218      return OpenCurrentFile();
219    }
220  }
221  return false;
222}
223
224bool FileRotatingStream::DisableBuffering() {
225  disable_buffering_ = true;
226  if (!file_stream_) {
227    std::cerr << "Open() must be called before DisableBuffering()."
228              << std::endl;
229    return false;
230  }
231  return file_stream_->DisableBuffering();
232}
233
234std::string FileRotatingStream::GetFilePath(size_t index) const {
235  RTC_DCHECK_LT(index, file_names_.size());
236  return file_names_[index];
237}
238
239bool FileRotatingStream::OpenCurrentFile() {
240  CloseCurrentFile();
241
242  // Opens the appropriate file in the appropriate mode.
243  RTC_DCHECK_LT(current_file_index_, file_names_.size());
244  std::string file_path = file_names_[current_file_index_];
245  file_stream_.reset(new FileStream());
246  const char* mode = nullptr;
247  switch (mode_) {
248    case kWrite:
249      mode = "w+";
250      // We should always we writing to the zero-th file.
251      RTC_DCHECK_EQ(current_file_index_, 0u);
252      break;
253    case kRead:
254      mode = "r";
255      break;
256  }
257  int error = 0;
258  if (!file_stream_->Open(file_path, mode, &error)) {
259    std::cerr << "Failed to open: " << file_path << "Error: " << error
260              << std::endl;
261    file_stream_.reset();
262    return false;
263  }
264  if (disable_buffering_) {
265    file_stream_->DisableBuffering();
266  }
267  return true;
268}
269
270void FileRotatingStream::CloseCurrentFile() {
271  if (!file_stream_) {
272    return;
273  }
274  current_bytes_written_ = 0;
275  file_stream_.reset();
276}
277
278void FileRotatingStream::RotateFiles() {
279  RTC_DCHECK_EQ(mode_, kWrite);
280  CloseCurrentFile();
281  // Rotates the files by deleting the file at |rotation_index_|, which is the
282  // oldest file and then renaming the newer files to have an incremented index.
283  // See header file comments for example.
284  RTC_DCHECK_LT(rotation_index_, file_names_.size());
285  std::string file_to_delete = file_names_[rotation_index_];
286  if (Filesystem::IsFile(file_to_delete)) {
287    if (!Filesystem::DeleteFile(file_to_delete)) {
288      std::cerr << "Failed to delete: " << file_to_delete << std::endl;
289    }
290  }
291  for (auto i = rotation_index_; i > 0; --i) {
292    std::string rotated_name = file_names_[i];
293    std::string unrotated_name = file_names_[i - 1];
294    if (Filesystem::IsFile(unrotated_name)) {
295      if (!Filesystem::MoveFile(unrotated_name, rotated_name)) {
296        std::cerr << "Failed to move: " << unrotated_name << " to "
297                  << rotated_name << std::endl;
298      }
299    }
300  }
301  // Create a new file for 0th index.
302  OpenCurrentFile();
303  OnRotation();
304}
305
306std::vector<std::string> FileRotatingStream::GetFilesWithPrefix() const {
307  std::vector<std::string> files;
308  // Iterate over the files in the directory.
309  DirectoryIterator it;
310  Pathname dir_path;
311  dir_path.SetFolder(dir_path_);
312  if (!it.Iterate(dir_path)) {
313    return files;
314  }
315  do {
316    std::string current_name = it.Name();
317    if (current_name.size() && !it.IsDirectory() &&
318        current_name.compare(0, file_prefix_.size(), file_prefix_) == 0) {
319      Pathname path(dir_path_, current_name);
320      files.push_back(path.pathname());
321    }
322  } while (it.Next());
323  return files;
324}
325
326std::string FileRotatingStream::GetFilePath(size_t index,
327                                            size_t num_files) const {
328  RTC_DCHECK_LT(index, num_files);
329  std::ostringstream file_name;
330  // The format will be "_%<num_digits>zu". We want to zero pad the index so
331  // that it will sort nicely.
332  size_t max_digits = ((num_files - 1) / 10) + 1;
333  size_t num_digits = (index / 10) + 1;
334  RTC_DCHECK_LE(num_digits, max_digits);
335  size_t padding = max_digits - num_digits;
336
337  file_name << file_prefix_ << "_";
338  for (size_t i = 0; i < padding; ++i) {
339    file_name << "0";
340  }
341  file_name << index;
342
343  Pathname file_path(dir_path_, file_name.str());
344  return file_path.pathname();
345}
346
347CallSessionFileRotatingStream::CallSessionFileRotatingStream(
348    const std::string& dir_path)
349    : FileRotatingStream(dir_path, kLogPrefix),
350      max_total_log_size_(0),
351      num_rotations_(0) {
352}
353
354CallSessionFileRotatingStream::CallSessionFileRotatingStream(
355    const std::string& dir_path,
356    size_t max_total_log_size)
357    : FileRotatingStream(dir_path,
358                         kLogPrefix,
359                         max_total_log_size / 2,
360                         GetNumRotatingLogFiles(max_total_log_size) + 1),
361      max_total_log_size_(max_total_log_size),
362      num_rotations_(0) {
363  RTC_DCHECK_GE(max_total_log_size, 4u);
364}
365
366const char* CallSessionFileRotatingStream::kLogPrefix = "webrtc_log";
367const size_t CallSessionFileRotatingStream::kRotatingLogFileDefaultSize =
368    1024 * 1024;
369
370void CallSessionFileRotatingStream::OnRotation() {
371  ++num_rotations_;
372  if (num_rotations_ == 1) {
373    // On the first rotation adjust the max file size so subsequent files after
374    // the first are smaller.
375    SetMaxFileSize(GetRotatingLogSize(max_total_log_size_));
376  } else if (num_rotations_ == (GetNumFiles() - 1)) {
377    // On the next rotation the very first file is going to be deleted. Change
378    // the rotation index so this doesn't happen.
379    SetRotationIndex(GetRotationIndex() - 1);
380  }
381}
382
383size_t CallSessionFileRotatingStream::GetRotatingLogSize(
384    size_t max_total_log_size) {
385  size_t num_rotating_log_files = GetNumRotatingLogFiles(max_total_log_size);
386  size_t rotating_log_size = num_rotating_log_files > 2
387                                 ? kRotatingLogFileDefaultSize
388                                 : max_total_log_size / 4;
389  return rotating_log_size;
390}
391
392size_t CallSessionFileRotatingStream::GetNumRotatingLogFiles(
393    size_t max_total_log_size) {
394  // At minimum have two rotating files. Otherwise split the available log size
395  // evenly across 1MB files.
396  return std::max((size_t)2,
397                  (max_total_log_size / 2) / kRotatingLogFileDefaultSize);
398}
399
400}  // namespace rtc
401