Commit 22a95fed authored by Nicolas Ouellet-Payeur's avatar Nicolas Ouellet-Payeur Committed by Commit Bot

[Extensions] Add ParallelUnpacker class

This class makes it easy to starts multiple SandboxedUnpacker's in
parallel, and get notified as soon as any of them completes. Since
SandboxedUnpacker is disk-bound and often takes >2s to run, running them
in parallel can be beneficial.

Right now ParallelUnpacker is not used anywhere, but a follow-up CL will
use it  to make extensions update in parallel, which will speed up
extension installs when many of them are queued at the same time (such
as for force-installed extensions).

Bug: 1103447
Change-Id: Ibc35588134fe0e91c4f7a243677181a08c642552
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2294563
Commit-Queue: Nicolas Ouellet-Payeur <nicolaso@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#791835}
parent a57817c7
...@@ -687,6 +687,10 @@ static_library("extensions") { ...@@ -687,6 +687,10 @@ static_library("extensions") {
"updater/extension_updater.h", "updater/extension_updater.h",
"updater/extension_updater_switches.cc", "updater/extension_updater_switches.cc",
"updater/extension_updater_switches.h", "updater/extension_updater_switches.h",
"updater/fetched_crx_file.cc",
"updater/fetched_crx_file.h",
"updater/parallel_unpacker.cc",
"updater/parallel_unpacker.h",
"user_script_listener.cc", "user_script_listener.cc",
"user_script_listener.h", "user_script_listener.h",
"warning_badge_service.cc", "warning_badge_service.cc",
......
...@@ -108,27 +108,6 @@ ExtensionUpdater::CheckParams::CheckParams( ...@@ -108,27 +108,6 @@ ExtensionUpdater::CheckParams::CheckParams(
ExtensionUpdater::CheckParams& ExtensionUpdater::CheckParams::operator=( ExtensionUpdater::CheckParams& ExtensionUpdater::CheckParams::operator=(
ExtensionUpdater::CheckParams&& other) = default; ExtensionUpdater::CheckParams&& other) = default;
ExtensionUpdater::FetchedCRXFile::FetchedCRXFile(
const CRXFileInfo& file,
bool file_ownership_passed,
const std::set<int>& request_ids,
InstallCallback callback)
: info(file),
file_ownership_passed(file_ownership_passed),
request_ids(request_ids),
callback(std::move(callback)) {}
ExtensionUpdater::FetchedCRXFile::FetchedCRXFile()
: file_ownership_passed(true) {}
ExtensionUpdater::FetchedCRXFile::FetchedCRXFile(FetchedCRXFile&& other) =
default;
ExtensionUpdater::FetchedCRXFile& ExtensionUpdater::FetchedCRXFile::operator=(
FetchedCRXFile&& other) = default;
ExtensionUpdater::FetchedCRXFile::~FetchedCRXFile() = default;
ExtensionUpdater::InProgressCheck::InProgressCheck() ExtensionUpdater::InProgressCheck::InProgressCheck()
: install_immediately(false), awaiting_update_service(false) {} : install_immediately(false), awaiting_update_service(false) {}
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/scoped_observer.h" #include "base/scoped_observer.h"
#include "chrome/browser/extensions/updater/fetched_crx_file.h"
#include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h" #include "content/public/browser/notification_registrar.h"
#include "extensions/browser/extension_registry_observer.h" #include "extensions/browser/extension_registry_observer.h"
...@@ -152,25 +153,6 @@ class ExtensionUpdater : public ExtensionDownloaderDelegate, ...@@ -152,25 +153,6 @@ class ExtensionUpdater : public ExtensionDownloaderDelegate,
friend class ExtensionUpdaterTest; friend class ExtensionUpdaterTest;
friend class ExtensionUpdaterFileHandler; friend class ExtensionUpdaterFileHandler;
// FetchedCRXFile holds information about a CRX file we fetched to disk,
// but have not yet installed.
struct FetchedCRXFile {
FetchedCRXFile();
FetchedCRXFile(const CRXFileInfo& file,
bool file_ownership_passed,
const std::set<int>& request_ids,
InstallCallback callback);
FetchedCRXFile(FetchedCRXFile&& other);
FetchedCRXFile& operator=(FetchedCRXFile&& other);
~FetchedCRXFile();
CRXFileInfo info;
GURL download_url;
bool file_ownership_passed;
std::set<int> request_ids;
InstallCallback callback;
};
struct InProgressCheck { struct InProgressCheck {
InProgressCheck(); InProgressCheck();
~InProgressCheck(); ~InProgressCheck();
......
// Copyright 2020 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/extensions/updater/fetched_crx_file.h"
namespace extensions {
FetchedCRXFile::FetchedCRXFile(
const CRXFileInfo& file,
bool file_ownership_passed,
const std::set<int>& request_ids,
ExtensionDownloaderDelegate::InstallCallback callback)
: info(file),
file_ownership_passed(file_ownership_passed),
request_ids(request_ids),
callback(std::move(callback)) {}
FetchedCRXFile::FetchedCRXFile() = default;
FetchedCRXFile::FetchedCRXFile(FetchedCRXFile&&) = default;
FetchedCRXFile& FetchedCRXFile::operator=(FetchedCRXFile&&) = default;
FetchedCRXFile::~FetchedCRXFile() = default;
} // namespace extensions
// Copyright 2020 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.
#ifndef CHROME_BROWSER_EXTENSIONS_UPDATER_FETCHED_CRX_FILE_H_
#define CHROME_BROWSER_EXTENSIONS_UPDATER_FETCHED_CRX_FILE_H_
#include <set>
#include "extensions/browser/crx_file_info.h"
#include "extensions/browser/updater/extension_downloader_delegate.h"
namespace extensions {
// FetchedCRXFile holds information about a CRX file we fetched to disk,
// but have not yet unpacked or installed.
struct FetchedCRXFile {
FetchedCRXFile();
FetchedCRXFile(const CRXFileInfo& file,
bool file_ownership_passed,
const std::set<int>& request_ids,
ExtensionDownloaderDelegate::InstallCallback callback);
FetchedCRXFile(FetchedCRXFile&& other);
FetchedCRXFile& operator=(FetchedCRXFile&&);
~FetchedCRXFile();
FetchedCRXFile(const FetchedCRXFile&) = delete;
FetchedCRXFile& operator=(const FetchedCRXFile&) = delete;
CRXFileInfo info;
GURL download_url;
bool file_ownership_passed = true;
std::set<int> request_ids;
ExtensionDownloaderDelegate::InstallCallback callback;
};
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_UPDATER_FETCHED_CRX_FILE_H_
// Copyright 2020 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/extensions/updater/parallel_unpacker.h"
#include "base/task/post_task.h"
#include "base/values.h"
#include "chrome/browser/extensions/pending_extension_info.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/content_verifier.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "third_party/skia/include/core/SkBitmap.h"
namespace extensions {
ParallelUnpacker::UnpackedExtension::UnpackedExtension() = default;
ParallelUnpacker::UnpackedExtension::UnpackedExtension(
FetchedCRXFile fetch_info,
const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
scoped_refptr<const Extension> extension,
const SkBitmap& install_icon,
declarative_net_request::RulesetChecksums ruleset_checksums)
: fetch_info(std::move(fetch_info)),
temp_dir(temp_dir),
extension_root(extension_root),
original_manifest(std::move(original_manifest)),
extension(extension),
install_icon(install_icon),
ruleset_checksums(std::move(ruleset_checksums)) {}
ParallelUnpacker::UnpackedExtension::UnpackedExtension(UnpackedExtension&&) =
default;
ParallelUnpacker::UnpackedExtension&
ParallelUnpacker::UnpackedExtension::operator=(UnpackedExtension&&) = default;
ParallelUnpacker::UnpackedExtension::~UnpackedExtension() = default;
ParallelUnpacker::ParallelUnpacker(Delegate* delegate, Profile* profile)
: delegate_(delegate), profile_(profile) {}
ParallelUnpacker::~ParallelUnpacker() = default;
void ParallelUnpacker::Unpack(
FetchedCRXFile fetch_info,
const PendingExtensionInfo* pending_extension_info,
const Extension* extension,
const base::FilePath& install_directory) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(pending_extension_info || extension);
auto install_source = pending_extension_info
? pending_extension_info->install_source()
: extension->location();
auto creation_flags = pending_extension_info
? pending_extension_info->creation_flags()
: Extension::NO_FLAGS;
auto io_task_runner = GetExtensionFileTaskRunner();
auto client = base::MakeRefCounted<Client>(
weak_ptr_factory_.GetWeakPtr(), std::move(fetch_info), io_task_runner);
auto unpacker = base::MakeRefCounted<SandboxedUnpacker>(
install_source, creation_flags, install_directory, io_task_runner,
client.get());
if (!io_task_runner->PostTask(
FROM_HERE, base::BindOnce(&SandboxedUnpacker::StartWithCrx, unpacker,
client->fetch_info().info))) {
NOTREACHED();
}
}
ParallelUnpacker::Client::Client(
base::WeakPtr<ParallelUnpacker> unpacker,
FetchedCRXFile fetch_info,
scoped_refptr<base::SequencedTaskRunner> io_task_runner)
: unpacker_(unpacker),
fetch_info_(std::move(fetch_info)),
io_task_runner_(io_task_runner) {}
ParallelUnpacker::Client::~Client() = default;
void ParallelUnpacker::Client::ShouldComputeHashesForOffWebstoreExtension(
scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback) {
DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
base::PostTask(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&ParallelUnpacker::Client::ShouldComputeHashesOnUIThread,
this, extension, std::move(callback)));
}
void ParallelUnpacker::Client::ShouldComputeHashesOnUIThread(
scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!unpacker_) {
// |ExtensionUpdater| isn't running, e.g. Stop() was called. Drop the refs
// in |callback|.
return;
}
auto* content_verifier =
ExtensionSystem::Get(unpacker_->profile_)->content_verifier();
bool result = content_verifier &&
content_verifier->ShouldComputeHashesOnInstall(*extension);
io_task_runner_->PostTask(FROM_HERE,
base::BindOnce(std::move(callback), result));
}
void ParallelUnpacker::Client::OnUnpackSuccess(
const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
const Extension* extension,
const SkBitmap& install_icon,
declarative_net_request::RulesetChecksums ruleset_checksums) {
DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
UnpackedExtension unpacked_extension(
std::move(fetch_info_), temp_dir, extension_root,
std::move(original_manifest), extension, install_icon,
std::move(ruleset_checksums));
base::PostTask(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&ParallelUnpacker::ReportSuccessOnUIThread,
unpacker_, std::move(unpacked_extension)));
}
void ParallelUnpacker::Client::OnUnpackFailure(const CrxInstallError& error) {
DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
base::PostTask(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&ParallelUnpacker::ReportFailureOnUIThread,
unpacker_, std::move(fetch_info_), error));
}
void ParallelUnpacker::ReportSuccessOnUIThread(
UnpackedExtension unpacked_extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
delegate_->OnParallelUnpackSuccess(std::move(unpacked_extension));
}
void ParallelUnpacker::ReportFailureOnUIThread(FetchedCRXFile fetch_info,
CrxInstallError error) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
delegate_->OnParallelUnpackFailure(std::move(fetch_info), std::move(error));
}
} // namespace extensions
// Copyright 2020 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.
#ifndef CHROME_BROWSER_EXTENSIONS_UPDATER_PARALLEL_UNPACKER_H_
#define CHROME_BROWSER_EXTENSIONS_UPDATER_PARALLEL_UNPACKER_H_
#include <memory>
#include <set>
#include "base/files/file_path.h"
#include "base/memory/scoped_refptr.h"
#include "chrome/browser/extensions/updater/fetched_crx_file.h"
#include "extensions/browser/api/declarative_net_request/ruleset_checksum.h"
#include "extensions/browser/crx_file_info.h"
#include "extensions/browser/install/crx_install_error.h"
#include "extensions/browser/sandboxed_unpacker.h"
#include "extensions/browser/updater/extension_downloader_delegate.h"
namespace base {
class DictionaryValue;
} // namespace base
class Profile;
namespace extensions {
class PendingExtensionInfo;
// Unpacks multiple extensions in parallel, and notifies |updater| when an
// extension has finished unpacking.
class ParallelUnpacker {
public:
// UnpackedExtension holds information about a CRX file we fetched and
// unpacked.
struct UnpackedExtension {
UnpackedExtension();
UnpackedExtension(
FetchedCRXFile fetch_info,
const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
scoped_refptr<const Extension> extension,
const SkBitmap& install_icon,
declarative_net_request::RulesetChecksums ruleset_checksums);
UnpackedExtension(UnpackedExtension&& other);
UnpackedExtension& operator=(UnpackedExtension&&);
~UnpackedExtension();
UnpackedExtension(const UnpackedExtension&) = delete;
UnpackedExtension& operator=(const UnpackedExtension&) = delete;
// Information about the fetched CRX file, including CRXFileInfo and a
// callback.
FetchedCRXFile fetch_info;
// The fields below are the result of
// SandboxedUnpackerClient::OnUnpackSuccess().
// Temporary directory with results of unpacking. It should be deleted once
// we don't need it anymore.
base::FilePath temp_dir;
// The path to the extension root inside of temp_dir.
base::FilePath extension_root;
// The parsed but unmodified version of the manifest, with no modifications
// such as localization, etc.
std::unique_ptr<base::DictionaryValue> original_manifest;
// The extension that was unpacked.
scoped_refptr<const Extension> extension;
// The icon we will display in the installation UI, if any.
SkBitmap install_icon;
// Checksums for the indexed rulesets corresponding to the Declarative Net
// Request API.
declarative_net_request::RulesetChecksums ruleset_checksums;
};
class Delegate {
public:
virtual void OnParallelUnpackSuccess(
UnpackedExtension unpacked_extension) = 0;
virtual void OnParallelUnpackFailure(FetchedCRXFile fetch_info,
CrxInstallError error) = 0;
};
// |delegate| must outlive this object.
ParallelUnpacker(Delegate* delegate, Profile* profile);
~ParallelUnpacker();
ParallelUnpacker(const ParallelUnpacker&) = delete;
ParallelUnpacker& operator=(const ParallelUnpacker&) = delete;
// Starts unpacking |crx_file|. Either |pending_extension_info| or |extension|
// must be non-null. When done unpacking, calls
// OnParallelUnpackSuccess/Failure() on this object's delegate.
//
// May be called multiple times in a row to unpack multiple extensions in
// parallel.
void Unpack(FetchedCRXFile crx_file,
const PendingExtensionInfo* pending_extension_info,
const Extension* extension,
const base::FilePath& install_directory);
private:
// Listens for a single SandboxedUnpacker's events. Routes
// OnUnpackSuccess/Failure() back to the ParallelUnpacker via
// ReportSuccess/FailureOnUIThread().
class Client : public SandboxedUnpackerClient {
public:
Client(base::WeakPtr<ParallelUnpacker> unpacker,
FetchedCRXFile fetch_info,
scoped_refptr<base::SequencedTaskRunner> io_task_runner);
// SandboxedUnpackerClient:
void ShouldComputeHashesForOffWebstoreExtension(
scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback) override;
void OnUnpackSuccess(
const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
const Extension* extension,
const SkBitmap& install_icon,
declarative_net_request::RulesetChecksums ruleset_checksums) override;
void OnUnpackFailure(const CrxInstallError& error) override;
FetchedCRXFile& fetch_info() { return fetch_info_; }
private:
~Client() override;
// To check whether we need to compute hashes or not, we have to make a
// query to ContentVerifier, and that should be done on the UI thread.
void ShouldComputeHashesOnUIThread(scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback);
base::WeakPtr<ParallelUnpacker> unpacker_;
FetchedCRXFile fetch_info_;
scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
};
void ReportSuccessOnUIThread(UnpackedExtension unpacked_extension);
void ReportFailureOnUIThread(FetchedCRXFile fetch_info,
CrxInstallError error);
Delegate* const delegate_;
Profile* const profile_;
base::WeakPtrFactory<ParallelUnpacker> weak_ptr_factory_{this};
};
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_UPDATER_PARALLEL_UNPACKER_H_
// Copyright 2020 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/extensions/updater/parallel_unpacker.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "chrome/browser/extensions/pending_extension_info.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/verifier_formats.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
namespace extensions {
class ParallelUnpackerTest : public testing::Test,
public ParallelUnpacker::Delegate {
public:
ParallelUnpackerTest()
: task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {}
void SetUp() override {
ASSERT_TRUE(extensions_dir_.CreateUniqueTempDir());
in_process_utility_thread_helper_.reset(
new content::InProcessUtilityThreadHelper);
parallel_unpacker_ = std::make_unique<ParallelUnpacker>(this, &profile_);
}
void TearDown() override {
in_process_utility_thread_helper_.reset();
parallel_unpacker_.reset();
}
base::FilePath GetCrxFullPath(const std::string& crx_name) {
base::FilePath full_path;
EXPECT_TRUE(base::PathService::Get(extensions::DIR_TEST_DATA, &full_path));
full_path = full_path.AppendASCII("unpacker").AppendASCII(crx_name);
EXPECT_TRUE(base::PathExists(full_path)) << full_path.value();
return full_path;
}
void Unpack(const std::string& crx_name) {
base::FilePath crx_path = GetCrxFullPath(crx_name);
extensions::CRXFileInfo crx_info(crx_path, GetTestVerifierFormat());
extensions::FetchedCRXFile fetch_info(crx_info, false, std::set<int>(),
base::BindOnce([](bool) {}));
extensions::PendingExtensionInfo pending_extension_info(
"", "", GURL(), base::Version(), [](const Extension*) { return true; },
false, Manifest::INTERNAL, Extension::NO_FLAGS, true, false);
parallel_unpacker_->Unpack(std::move(fetch_info), &pending_extension_info,
nullptr, extensions_dir_.GetPath());
in_progress_count_++;
}
// ParallelUnpacker::Delegate:
void OnParallelUnpackSuccess(
ParallelUnpacker::UnpackedExtension unpacked_extension) override {
std::string file_name =
unpacked_extension.fetch_info.info.path.BaseName().MaybeAsASCII();
successful_unpacks_.emplace(file_name, std::move(unpacked_extension));
if (--in_progress_count_ == 0)
std::move(quit_closure_).Run();
}
void OnParallelUnpackFailure(FetchedCRXFile fetch_info,
CrxInstallError error) override {
std::string file_name = fetch_info.info.path.BaseName().MaybeAsASCII();
failed_unpacks_.emplace(file_name, std::move(error));
if (--in_progress_count_ == 0)
std::move(quit_closure_).Run();
}
void WaitForAllComplete() {
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
protected:
std::map<std::string, ParallelUnpacker::UnpackedExtension>
successful_unpacks_;
std::map<std::string, CrxInstallError> failed_unpacks_;
private:
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
base::ScopedTempDir extensions_dir_;
std::unique_ptr<content::InProcessUtilityThreadHelper>
in_process_utility_thread_helper_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
std::unique_ptr<ParallelUnpacker> parallel_unpacker_;
base::OnceClosure quit_closure_;
int in_progress_count_ = 0;
};
TEST_F(ParallelUnpackerTest, OneGood) {
Unpack("good_package.crx");
WaitForAllComplete();
EXPECT_EQ(successful_unpacks_.size(), 1u);
EXPECT_EQ(failed_unpacks_.size(), 0u);
}
TEST_F(ParallelUnpackerTest, TwoGoodInParallel) {
Unpack("good_package.crx");
Unpack("good_l10n.crx");
WaitForAllComplete();
EXPECT_EQ(successful_unpacks_.size(), 2u);
EXPECT_EQ(failed_unpacks_.size(), 0u);
}
TEST_F(ParallelUnpackerTest, OneGoodAndOneBadInParallel) {
Unpack("good_package.crx");
Unpack("missing_default_data.crx");
WaitForAllComplete();
EXPECT_EQ(successful_unpacks_.size(), 1u);
EXPECT_EQ(failed_unpacks_.size(), 1u);
EXPECT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
failed_unpacks_.find("missing_default_data.crx")->second.type());
}
} // namespace extensions
...@@ -4869,6 +4869,7 @@ test("unit_tests") { ...@@ -4869,6 +4869,7 @@ test("unit_tests") {
"../browser/extensions/update_install_gate_unittest.cc", "../browser/extensions/update_install_gate_unittest.cc",
"../browser/extensions/updater/extension_update_client_command_line_config_policy_unittest.cc", "../browser/extensions/updater/extension_update_client_command_line_config_policy_unittest.cc",
"../browser/extensions/updater/extension_updater_unittest.cc", "../browser/extensions/updater/extension_updater_unittest.cc",
"../browser/extensions/updater/parallel_unpacker_unittest.cc",
"../browser/extensions/user_script_listener_unittest.cc", "../browser/extensions/user_script_listener_unittest.cc",
"../browser/extensions/warning_badge_service_unittest.cc", "../browser/extensions/warning_badge_service_unittest.cc",
"../browser/extensions/webstore_installer_unittest.cc", "../browser/extensions/webstore_installer_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment