Commit 53c12a22 authored by James West's avatar James West Committed by Commit Bot

[Chromecast] Application-specific command-line switches

Allow individual Cast web applications to set command-line switches
for the render process.

Bug: internal b/67735190
Bug: internal b/77323820
Test: cast_shell_browsertests, cast_shell_unittests
Change-Id: I2fdeca54e9f4a46f6299c58482e9934ebffe8b69
Reviewed-on: https://chromium-review.googlesource.com/1018205
Commit-Queue: James West <jameswest@chromium.org>
Reviewed-by: default avatarLuke Halliwell <halliwell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#558077}
parent cb9414a0
......@@ -73,6 +73,8 @@ cast_source_set("browser") {
"network_context_manager.h",
"pref_service_helper.cc",
"pref_service_helper.h",
"renderer_config.cc",
"renderer_config.h",
"renderer_prelauncher.cc",
"renderer_prelauncher.h",
"service/cast_service_simple.cc",
......@@ -410,6 +412,7 @@ cast_source_set("unittests") {
"devtools/cast_devtools_manager_delegate_unittest.cc",
"lru_renderer_cache_test.cc",
"network_context_manager_unittest.cc",
"renderer_config_unittest.cc",
]
deps = [
......
......@@ -34,6 +34,7 @@
#include "chromecast/browser/devtools/cast_devtools_manager_delegate.h"
#include "chromecast/browser/grit/cast_browser_resources.h"
#include "chromecast/browser/media/media_caps_impl.h"
#include "chromecast/browser/renderer_config.h"
#include "chromecast/browser/service/cast_service_simple.h"
#include "chromecast/browser/url_request_context_factory.h"
#include "chromecast/common/global_descriptors.h"
......@@ -158,7 +159,8 @@ GetRequestContextGetterFromBrowserContext() {
CastContentBrowserClient::CastContentBrowserClient()
: cast_browser_main_parts_(nullptr),
url_request_context_factory_(new URLRequestContextFactory()) {}
url_request_context_factory_(new URLRequestContextFactory()),
renderer_config_manager_(std::make_unique<RendererConfigManager>()) {}
CastContentBrowserClient::~CastContentBrowserClient() {
content::BrowserThread::DeleteSoon(content::BrowserThread::IO, FROM_HERE,
......@@ -457,6 +459,12 @@ void CastContentBrowserClient::AppendExtraCommandLineSwitches(
}
#endif // defined(USE_AURA)
}
auto renderer_config =
renderer_config_manager_->GetRendererConfig(child_process_id);
if (renderer_config) {
renderer_config->AppendSwitchesTo(command_line);
}
}
void CastContentBrowserClient::OverrideWebkitPrefs(
......
......@@ -55,9 +55,9 @@ class VideoResolutionPolicy;
}
namespace shell {
class CastBrowserMainParts;
class CastResourceDispatcherHostDelegate;
class RendererConfigManager;
class URLRequestContextFactory;
class CastContentBrowserClient : public content::ContentBrowserClient {
......@@ -189,6 +189,10 @@ class CastContentBrowserClient : public content::ContentBrowserClient {
std::unique_ptr<content::NavigationUIData> GetNavigationUIData(
content::NavigationHandle* navigation_handle) override;
RendererConfigManager* renderer_config_manager() const {
return renderer_config_manager_.get();
}
protected:
CastContentBrowserClient();
......@@ -234,6 +238,7 @@ class CastContentBrowserClient : public content::ContentBrowserClient {
std::unique_ptr<CastResourceDispatcherHostDelegate>
resource_dispatcher_host_delegate_;
std::unique_ptr<media::CmaBackendFactory> cma_backend_factory_;
std::unique_ptr<RendererConfigManager> renderer_config_manager_;
DISALLOW_COPY_AND_ASSIGN(CastContentBrowserClient);
};
......
......@@ -8,18 +8,23 @@
#include "base/bind.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "chromecast/browser/renderer_config.h"
#include "chromecast/browser/renderer_prelauncher.h"
#include "content/public/browser/site_instance.h"
namespace chromecast {
LRURendererCache::LRURendererCache(content::BrowserContext* browser_context,
size_t max_renderers)
LRURendererCache::LRURendererCache(
content::BrowserContext* browser_context,
shell::RendererConfigManager* renderer_config_manager,
size_t max_renderers)
: browser_context_(browser_context),
renderer_config_manager_(renderer_config_manager),
max_renderers_(max_renderers),
in_use_count_(0),
weak_factory_(this) {
DCHECK(browser_context_);
DCHECK(renderer_config_manager_);
memory_pressure_listener_ =
std::make_unique<base::MemoryPressureListener>(base::BindRepeating(
&LRURendererCache::OnMemoryPressure, weak_factory_.GetWeakPtr()));
......@@ -95,8 +100,9 @@ void LRURendererCache::StartNextPrelauncher(const GURL& page_url) {
if (factory_for_testing_) {
cache_.push_front(factory_for_testing_->Create(browser_context_, page_url));
} else {
cache_.push_front(
std::make_unique<RendererPrelauncher>(browser_context_, page_url));
cache_.push_front(std::make_unique<RendererPrelauncher>(
browser_context_,
renderer_config_manager_->CreateRendererConfigurator(), page_url));
}
// Evict the cache before prelaunching.
EvictCache();
......
......@@ -21,6 +21,10 @@ class BrowserContext;
namespace chromecast {
class RendererPrelauncher;
namespace shell {
class RendererConfigManager;
} // namespace shell
// Factory class for testing.
class RendererPrelauncherFactory {
public:
......@@ -35,6 +39,7 @@ class RendererPrelauncherFactory {
class LRURendererCache {
public:
LRURendererCache(content::BrowserContext* browser_context,
shell::RendererConfigManager* renderer_config_manager,
size_t max_renderers);
virtual ~LRURendererCache();
......@@ -64,6 +69,7 @@ class LRURendererCache {
void EvictCache();
content::BrowserContext* const browser_context_;
shell::RendererConfigManager* const renderer_config_manager_;
const size_t max_renderers_;
size_t in_use_count_;
std::list<std::unique_ptr<RendererPrelauncher>> cache_;
......
......@@ -5,8 +5,10 @@
#include "chromecast/browser/lru_renderer_cache.h"
#include <memory>
#include <utility>
#include "base/memory/ptr_util.h"
#include "chromecast/browser/renderer_config.h"
#include "chromecast/browser/renderer_prelauncher.h"
#include "content/public/browser/site_instance.h"
#include "content/public/test/test_browser_context.h"
......@@ -15,10 +17,12 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#define EXPECT_CREATE_AND_PRELAUNCH(ptr, url) \
ptr = new MockPrelauncher(&browser_context_, url); \
EXPECT_CALL(*ptr, Prelaunch()); \
EXPECT_CALL(factory_, Create(&browser_context_, url)) \
#define EXPECT_CREATE_AND_PRELAUNCH(ptr, url) \
ptr = new MockPrelauncher( \
&browser_context_, \
renderer_config_manager_.CreateRendererConfigurator(), url); \
EXPECT_CALL(*ptr, Prelaunch()); \
EXPECT_CALL(factory_, Create(&browser_context_, url)) \
.WillOnce(Return(ByMove(std::unique_ptr<MockPrelauncher>(ptr))));
#define EXPECT_EVICTION(ptr) EXPECT_CALL(*ptr, Destroy());
......@@ -43,8 +47,11 @@ const GURL kUrl3("https://www.three.com");
class MockPrelauncher : public RendererPrelauncher {
public:
MockPrelauncher(content::BrowserContext* browser_context,
shell::RendererConfigurator renderer_configurator,
const GURL& page_url)
: RendererPrelauncher(browser_context, page_url) {}
: RendererPrelauncher(browser_context,
std::move(renderer_configurator),
page_url) {}
virtual ~MockPrelauncher() { Destroy(); }
MOCK_METHOD0(Prelaunch, void());
......@@ -71,11 +78,13 @@ class LRURendererCacheTest : public testing::Test {
content::TestBrowserThreadBundle threads_;
content::TestBrowserContext browser_context_;
MockFactory factory_;
shell::RendererConfigManager renderer_config_manager_;
std::unique_ptr<LRURendererCache> lru_cache_;
};
TEST_F(LRURendererCacheTest, SimpleTakeAndRelease) {
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_,
&renderer_config_manager_, 1);
SetFactory();
MockPrelauncher* p1;
std::unique_ptr<RendererPrelauncher> taken;
......@@ -110,7 +119,8 @@ TEST_F(LRURendererCacheTest, SimpleTakeAndRelease) {
}
TEST_F(LRURendererCacheTest, SimpleCacheEviction) {
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_,
&renderer_config_manager_, 1);
SetFactory();
MockPrelauncher* p1;
std::unique_ptr<RendererPrelauncher> taken;
......@@ -135,7 +145,8 @@ TEST_F(LRURendererCacheTest, SimpleCacheEviction) {
}
TEST_F(LRURendererCacheTest, CapacityOne) {
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_,
&renderer_config_manager_, 1);
SetFactory();
MockPrelauncher* p1;
MockPrelauncher* p2;
......@@ -222,7 +233,8 @@ TEST_F(LRURendererCacheTest, CapacityOne) {
}
TEST_F(LRURendererCacheTest, CapacityTwo) {
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 2);
lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_,
&renderer_config_manager_, 2);
SetFactory();
MockPrelauncher* p1;
MockPrelauncher* p2;
......
// Copyright 2018 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 "chromecast/browser/renderer_config.h"
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "content/public/common/child_process_host.h"
namespace chromecast {
namespace shell {
namespace {
class RendererConfigImpl : public RendererConfig {
public:
RendererConfigImpl(int render_process_id,
base::flat_set<std::string> switches,
base::flat_map<std::string, std::string> ascii_switches)
: render_process_id_(render_process_id),
switches_(std::move(switches)),
ascii_switches_(std::move(ascii_switches)) {
DCHECK_NE(content::ChildProcessHost::kInvalidUniqueID, render_process_id);
}
int GetRenderProcessId() const override { return render_process_id_; }
void AppendSwitchesTo(base::CommandLine* command_line) const override {
DCHECK(command_line);
for (const std::string& switch_string : switches_) {
command_line->AppendSwitch(switch_string);
}
for (const auto& ascii_switch : ascii_switches_) {
command_line->AppendSwitchASCII(ascii_switch.first, ascii_switch.second);
}
}
private:
~RendererConfigImpl() override = default;
const int render_process_id_;
const base::flat_set<std::string> switches_;
const base::flat_map<std::string /* switch */, std::string /* value */>
ascii_switches_;
DISALLOW_COPY_AND_ASSIGN(RendererConfigImpl);
};
} // namespace
RendererConfig::RendererConfig() = default;
RendererConfig::~RendererConfig() = default;
RendererConfigurator::RendererConfigurator(
AddRendererConfigCallback add_renderer_config_callback,
RemoveRendererConfigCallback remove_renderer_config_callback)
: add_renderer_config_callback_(std::move(add_renderer_config_callback)),
remove_renderer_config_callback_(
std::move(remove_renderer_config_callback)) {
DCHECK(add_renderer_config_callback_);
DCHECK(remove_renderer_config_callback_);
}
RendererConfigurator::RendererConfigurator(RendererConfigurator&& other)
: add_renderer_config_callback_(
std::move(other.add_renderer_config_callback_)),
remove_renderer_config_callback_(
std::move(other.remove_renderer_config_callback_)),
render_process_id_(other.render_process_id_),
switches_(std::move(other.switches_)),
ascii_switches_(std::move(other.ascii_switches_)) {
other.render_process_id_ = content::ChildProcessHost::kInvalidUniqueID;
}
RendererConfigurator::~RendererConfigurator() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!add_renderer_config_callback_ && remove_renderer_config_callback_) {
std::move(remove_renderer_config_callback_).Run(render_process_id_);
}
}
void RendererConfigurator::Configure(int render_process_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(add_renderer_config_callback_);
DCHECK_NE(content::ChildProcessHost::kInvalidUniqueID, render_process_id);
render_process_id_ = render_process_id;
std::move(add_renderer_config_callback_)
.Run(render_process_id_, base::MakeRefCounted<RendererConfigImpl>(
render_process_id, std::move(switches_),
std::move(ascii_switches_)));
}
void RendererConfigurator::AppendSwitch(const std::string& switch_string) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(add_renderer_config_callback_);
switches_.insert(switch_string);
}
void RendererConfigurator::AppendSwitchASCII(const std::string& switch_string,
const std::string& value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(add_renderer_config_callback_);
ascii_switches_[switch_string] = value;
}
RendererConfigManager::RendererConfigManager() : weak_factory_(this) {}
RendererConfigManager::~RendererConfigManager() = default;
RendererConfigurator RendererConfigManager::CreateRendererConfigurator() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return RendererConfigurator(
base::BindOnce(&RendererConfigManager::AddRendererConfig,
weak_factory_.GetWeakPtr()),
base::BindOnce(&RendererConfigManager::RemoveRendererConfig,
weak_factory_.GetWeakPtr()));
}
scoped_refptr<const RendererConfig> RendererConfigManager::GetRendererConfig(
int render_process_id) {
if (render_process_id == content::ChildProcessHost::kInvalidUniqueID) {
return nullptr;
}
base::AutoLock lock(renderer_configs_lock_);
auto it = renderer_configs_.find(render_process_id);
if (it == renderer_configs_.end()) {
return nullptr;
}
return it->second;
}
void RendererConfigManager::AddRendererConfig(
int render_process_id,
scoped_refptr<RendererConfig> renderer_config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(content::ChildProcessHost::kInvalidUniqueID, render_process_id);
base::AutoLock lock(renderer_configs_lock_);
DCHECK_EQ(0U, renderer_configs_.count(render_process_id));
renderer_configs_.emplace(render_process_id, std::move(renderer_config));
}
void RendererConfigManager::RemoveRendererConfig(int render_process_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::AutoLock lock(renderer_configs_lock_);
DCHECK_NE(0U, renderer_configs_.count(render_process_id));
renderer_configs_.erase(render_process_id);
}
} // namespace shell
} // namespace chromecast
// Copyright 2018 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 CHROMECAST_BROWSER_RENDERER_CONFIG_H_
#define CHROMECAST_BROWSER_RENDERER_CONFIG_H_
#include <string>
#include "base/callback.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/synchronization/lock.h"
namespace base {
class CommandLine;
} // namespace base
namespace chromecast {
namespace shell {
// Application-specific configuration for the renderer.
// All methods are thread-safe.
class RendererConfig : public base::RefCountedThreadSafe<RendererConfig> {
public:
REQUIRE_ADOPTION_FOR_REFCOUNTED_TYPE();
RendererConfig();
// Returns the render process ID.
virtual int GetRenderProcessId() const = 0;
// Appends all switches to |command line|.
virtual void AppendSwitchesTo(base::CommandLine* command_line) const = 0;
protected:
virtual ~RendererConfig();
private:
friend class base::RefCountedThreadSafe<RendererConfig>;
DISALLOW_COPY_AND_ASSIGN(RendererConfig);
};
// Used to configure the renderer for individual applications.
// All methods must be called on the main thread.
class RendererConfigurator {
public:
using AddRendererConfigCallback =
base::OnceCallback<void(int render_process_id,
scoped_refptr<RendererConfig> renderer_config)>;
using RemoveRendererConfigCallback =
base::OnceCallback<void(int render_process_id)>;
RendererConfigurator(
AddRendererConfigCallback add_renderer_config_callback,
RemoveRendererConfigCallback remove_renderer_config_callback);
RendererConfigurator(RendererConfigurator&& other);
virtual ~RendererConfigurator();
// Configures the renderer with |render_process_id|. Must only be called once,
// and no other methods may be called afterward. Must be called before the
// render process is started.
void Configure(int render_process_id);
// Appends a switch, with an optional value, to the command line.
void AppendSwitch(const std::string& switch_string);
void AppendSwitchASCII(const std::string& switch_string,
const std::string& value);
private:
AddRendererConfigCallback add_renderer_config_callback_;
RemoveRendererConfigCallback remove_renderer_config_callback_;
int render_process_id_;
base::flat_set<std::string> switches_;
base::flat_map<std::string /* switch */, std::string /* value */>
ascii_switches_;
SEQUENCE_CHECKER(sequence_checker_);
DISALLOW_COPY_AND_ASSIGN(RendererConfigurator);
};
class RendererConfigManager {
public:
RendererConfigManager();
~RendererConfigManager();
// Returns a new renderer configurator.
// Must be called on the main thread.
RendererConfigurator CreateRendererConfigurator();
// Returns the config for the renderer with |render_process_id|.
// May be called on any thread.
scoped_refptr<const RendererConfig> GetRendererConfig(int render_process_id);
private:
void AddRendererConfig(int render_process_id,
scoped_refptr<RendererConfig> renderer_config);
void RemoveRendererConfig(int render_process_id);
base::flat_map<int /* render_process_id */, scoped_refptr<RendererConfig>>
renderer_configs_;
base::Lock renderer_configs_lock_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<RendererConfigManager> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(RendererConfigManager);
};
} // namespace shell
} // namespace chromecast
#endif // CHROMECAST_BROWSER_RENDERER_CONFIG_H_
// Copyright 2018 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 "chromecast/browser/renderer_config.h"
#include <memory>
#include "base/command_line.h"
#include "base/memory/ref_counted.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromecast {
namespace shell {
namespace {
constexpr char kSwitch1[] = "switch1";
constexpr char kSwitch2[] = "switch2";
constexpr char kValue[] = "value";
constexpr int kRenderProcessId = 123;
} // namespace
TEST(RendererConfigTest, AppendCommandLineSwitches) {
RendererConfigManager manager;
ASSERT_FALSE(manager.GetRendererConfig(kRenderProcessId));
scoped_refptr<const RendererConfig> config;
{
auto configurator = manager.CreateRendererConfigurator();
configurator.AppendSwitch(kSwitch1);
configurator.AppendSwitchASCII(kSwitch2, kValue);
configurator.Configure(kRenderProcessId);
config = manager.GetRendererConfig(kRenderProcessId);
ASSERT_TRUE(config);
}
EXPECT_FALSE(manager.GetRendererConfig(kRenderProcessId));
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
config->AppendSwitchesTo(&command_line);
EXPECT_TRUE(command_line.HasSwitch(kSwitch1));
EXPECT_TRUE(command_line.HasSwitch(kSwitch2));
EXPECT_EQ(kValue, command_line.GetSwitchValueASCII(kSwitch2));
}
TEST(RendererConfigTest, ConfiguratorOutlivesManager) {
auto manager = std::make_unique<RendererConfigManager>();
ASSERT_FALSE(manager->GetRendererConfig(kRenderProcessId));
auto configurator = manager->CreateRendererConfigurator();
configurator.Configure(kRenderProcessId);
EXPECT_TRUE(manager->GetRendererConfig(kRenderProcessId));
manager.reset();
}
TEST(RendererConfigTest, ConfigureAfterManagerDestroyed) {
auto manager = std::make_unique<RendererConfigManager>();
auto configurator = manager->CreateRendererConfigurator();
manager.reset();
configurator.Configure(kRenderProcessId);
}
} // namespace shell
} // namespace chromecast
......@@ -4,6 +4,8 @@
#include "chromecast/browser/renderer_prelauncher.h"
#include <utility>
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/common/child_process_host.h"
......@@ -12,8 +14,10 @@ namespace chromecast {
RendererPrelauncher::RendererPrelauncher(
content::BrowserContext* browser_context,
shell::RendererConfigurator renderer_configurator,
const GURL& gurl)
: browser_context_(browser_context),
renderer_configurator_(std::move(renderer_configurator)),
gurl_(gurl),
rph_routing_id_(MSG_ROUTING_NONE) {}
......@@ -27,6 +31,7 @@ RendererPrelauncher::~RendererPrelauncher() {
void RendererPrelauncher::Prelaunch() {
DLOG(INFO) << "Prelaunching for: " << gurl_;
site_instance_ = content::SiteInstance::CreateForURL(browser_context_, gurl_);
renderer_configurator_.Configure(site_instance_->GetProcess()->GetID());
content::RenderProcessHost* rph = site_instance_->GetProcess();
rph_routing_id_ = rph->GetNextRoutingID();
rph->AddRoute(rph_routing_id_, this);
......
......@@ -7,6 +7,7 @@
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "chromecast/browser/renderer_config.h"
#include "ipc/ipc_listener.h"
#include "url/gurl.h"
......@@ -14,7 +15,7 @@ namespace content {
class BrowserContext;
class RenderProcessHost;
class SiteInstance;
}
} // namespace content
namespace chromecast {
......@@ -25,6 +26,7 @@ namespace chromecast {
class RendererPrelauncher : private IPC::Listener {
public:
RendererPrelauncher(content::BrowserContext* browser_context,
shell::RendererConfigurator renderer_configurator,
const GURL& gurl);
~RendererPrelauncher() override;
......@@ -41,6 +43,7 @@ class RendererPrelauncher : private IPC::Listener {
bool OnMessageReceived(const IPC::Message& message) override;
content::BrowserContext* const browser_context_;
shell::RendererConfigurator renderer_configurator_;
scoped_refptr<content::SiteInstance> site_instance_;
const GURL gurl_;
int32_t rph_routing_id_;
......
......@@ -13,6 +13,7 @@
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "chromecast/browser/cast_browser_context.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/renderer_config.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
......@@ -59,8 +60,10 @@ IN_PROC_BROWSER_TEST_F(RendererPrelauncherTest, ReusedRenderer) {
EXPECT_TRUE(browser_context);
// Prelaunch a renderer process for the url.
std::unique_ptr<RendererPrelauncher> prelauncher(
new RendererPrelauncher(browser_context, gurl));
shell::RendererConfigManager renderer_config_manager;
auto prelauncher = std::make_unique<RendererPrelauncher>(
browser_context, renderer_config_manager.CreateRendererConfigurator(),
gurl);
prelauncher->Prelaunch();
scoped_refptr<content::SiteInstance> site_instance =
prelauncher->site_instance();
......
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