| // Copyright 2019 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 "fuchsia/engine/browser/web_engine_devtools_controller.h" |
| |
| #include <chromium/internal/cpp/fidl.h> |
| #include <fuchsia/web/cpp/fidl.h> |
| #include <lib/fidl/cpp/interface_ptr_set.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/containers/flat_set.h" |
| #include "base/fuchsia/process_context.h" |
| #include "base/fuchsia/scoped_service_binding.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_tokenizer.h" |
| #include "content/public/browser/devtools_agent_host.h" |
| #include "content/public/browser/devtools_socket_factory.h" |
| #include "content/public/common/content_switches.h" |
| #include "fuchsia/engine/switches.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/port_util.h" |
| #include "net/socket/tcp_server_socket.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| namespace { |
| |
| using OnDevToolsPortChanged = base::OnceCallback<void(uint16_t)>; |
| |
| class DevToolsSocketFactory : public content::DevToolsSocketFactory { |
| public: |
| DevToolsSocketFactory(OnDevToolsPortChanged on_devtools_port, |
| net::IPEndPoint ip_end_point) |
| : on_devtools_port_(std::move(on_devtools_port)), |
| ip_end_point_(std::move(ip_end_point)) {} |
| ~DevToolsSocketFactory() override = default; |
| |
| DevToolsSocketFactory(const DevToolsSocketFactory&) = delete; |
| DevToolsSocketFactory& operator=(const DevToolsSocketFactory&) = delete; |
| |
| // content::DevToolsSocketFactory implementation. |
| std::unique_ptr<net::ServerSocket> CreateForHttpServer() override { |
| const int kTcpListenBackLog = 5; |
| auto socket = |
| std::make_unique<net::TCPServerSocket>(nullptr, net::NetLogSource()); |
| int error = socket->Listen(ip_end_point_, kTcpListenBackLog); |
| if (error != net::OK) { |
| LOG(WARNING) << "Failed to start the HTTP debugger service. " |
| << net::ErrorToString(error); |
| std::move(on_devtools_port_).Run(0); |
| return nullptr; |
| } |
| |
| net::IPEndPoint end_point; |
| socket->GetLocalAddress(&end_point); |
| std::move(on_devtools_port_).Run(end_point.port()); |
| return socket; |
| } |
| |
| std::unique_ptr<net::ServerSocket> CreateForTethering( |
| std::string* out_name) override { |
| return nullptr; |
| } |
| |
| private: |
| OnDevToolsPortChanged on_devtools_port_; |
| net::IPEndPoint ip_end_point_; |
| }; |
| |
| void StartRemoteDebuggingServer(OnDevToolsPortChanged on_devtools_port, |
| net::IPEndPoint ip_end_point) { |
| const base::FilePath kDisableActivePortOutputDirectory{}; |
| const base::FilePath kDisableDebugOutput{}; |
| |
| content::DevToolsAgentHost::StartRemoteDebuggingServer( |
| std::make_unique<DevToolsSocketFactory>(std::move(on_devtools_port), |
| ip_end_point), |
| kDisableActivePortOutputDirectory, kDisableDebugOutput); |
| } |
| |
| class NoopController : public WebEngineDevToolsController { |
| public: |
| NoopController() = default; |
| ~NoopController() override = default; |
| |
| NoopController(const NoopController&) = delete; |
| NoopController& operator=(const NoopController&) = delete; |
| |
| // WebEngineDevToolsController implementation: |
| bool OnFrameCreated(content::WebContents* contents, |
| bool user_debugging) override { |
| return !user_debugging; |
| } |
| void OnFrameLoaded(content::WebContents* contents) override {} |
| void OnFrameDestroyed(content::WebContents* contents) override {} |
| content::DevToolsAgentHost::List RemoteDebuggingTargets() override { |
| return {}; |
| } |
| void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override { |
| std::move(callback).Run(0); |
| } |
| }; |
| |
| // "User-mode" makes DevTools accessible to remote devices for Frames specified |
| // by the web_instance owner. The controller, which starts DevTools when the |
| // first Frame is created, and shuts it down when no debuggable Frames remain. |
| class UserModeController : public WebEngineDevToolsController { |
| public: |
| explicit UserModeController(uint16_t server_port) |
| : ip_endpoint_(net::IPAddress::IPv6AllZeros(), server_port) {} |
| ~UserModeController() override { |
| if (is_remote_debugging_started_) { |
| content::DevToolsAgentHost::StopRemoteDebuggingServer(); |
| } |
| } |
| |
| UserModeController(const UserModeController&) = delete; |
| UserModeController& operator=(const UserModeController&) = delete; |
| |
| // WebEngineDevToolsController implementation: |
| bool OnFrameCreated(content::WebContents* contents, |
| bool user_debugging) override { |
| if (user_debugging) { |
| debuggable_contents_.insert(contents); |
| UpdateRemoteDebuggingServer(); |
| } |
| return true; |
| } |
| void OnFrameLoaded(content::WebContents* contents) override {} |
| void OnFrameDestroyed(content::WebContents* contents) override { |
| debuggable_contents_.erase(contents); |
| UpdateRemoteDebuggingServer(); |
| } |
| content::DevToolsAgentHost::List RemoteDebuggingTargets() override { |
| DCHECK(is_remote_debugging_started_); |
| |
| content::DevToolsAgentHost::List enabled_hosts; |
| for (content::WebContents* contents : debuggable_contents_) { |
| enabled_hosts.push_back( |
| content::DevToolsAgentHost::GetOrCreateFor(contents)); |
| } |
| return enabled_hosts; |
| } |
| void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override { |
| get_port_callbacks_.emplace_back(std::move(callback)); |
| MaybeNotifyGetPortCallbacks(); |
| } |
| |
| private: |
| // Starts or stops the remote debugging server, if necessary |
| void UpdateRemoteDebuggingServer() { |
| bool need_remote_debugging = !debuggable_contents_.empty(); |
| if (need_remote_debugging == is_remote_debugging_started_) |
| return; |
| is_remote_debugging_started_ = need_remote_debugging; |
| |
| if (need_remote_debugging) { |
| StartRemoteDebuggingServer( |
| base::BindOnce(&UserModeController::OnDevToolsPortChanged, |
| base::Unretained(this)), |
| ip_endpoint_); |
| } else { |
| content::DevToolsAgentHost::StopRemoteDebuggingServer(); |
| devtools_port_.reset(); |
| } |
| } |
| |
| void OnDevToolsPortChanged(uint16_t port) { |
| devtools_port_ = port; |
| MaybeNotifyGetPortCallbacks(); |
| } |
| |
| void MaybeNotifyGetPortCallbacks() { |
| if (!devtools_port_.has_value()) |
| return; |
| for (auto& callback : get_port_callbacks_) |
| std::move(callback).Run(devtools_port_.value()); |
| get_port_callbacks_.clear(); |
| } |
| |
| const net::IPEndPoint ip_endpoint_; |
| |
| // True if the remote debugging server is started. |
| bool is_remote_debugging_started_ = false; |
| |
| // Currently active DevTools port. Set to 0 on service startup error. |
| absl::optional<uint16_t> devtools_port_; |
| |
| // Set of Frames' content::WebContents which are remotely debuggable. |
| base::flat_set<content::WebContents*> debuggable_contents_; |
| |
| std::vector<base::OnceCallback<void(uint16_t)>> get_port_callbacks_; |
| }; |
| |
| // "Debug-mode" is used for on-device testing, and makes all Frames available |
| // for debugging by clients on the same device. DevTools is only reported when |
| // the first Frame finishes loading its main document, so that the |
| // DevToolsPerContextListeners can start interacting with it immediately. |
| class DebugModeController : public WebEngineDevToolsController, |
| public chromium::internal::DevToolsConnector { |
| public: |
| DebugModeController() |
| : DebugModeController( |
| net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0)) {} |
| ~DebugModeController() override { |
| content::DevToolsAgentHost::StopRemoteDebuggingServer(); |
| } |
| |
| DebugModeController(const DebugModeController&) = delete; |
| DebugModeController& operator=(const DebugModeController&) = delete; |
| |
| // DevToolsController implementation: |
| bool OnFrameCreated(content::WebContents* contents, |
| bool user_debugging) override { |
| return !user_debugging; |
| } |
| void OnFrameLoaded(content::WebContents* contents) override { |
| frame_loaded_ = true; |
| MaybeSendRemoteDebuggingCallbacks(); |
| } |
| void OnFrameDestroyed(content::WebContents* contents) override {} |
| content::DevToolsAgentHost::List RemoteDebuggingTargets() override { |
| return content::DevToolsAgentHost::GetOrCreateAll(); |
| } |
| void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override { |
| std::move(callback).Run(0); |
| } |
| |
| protected: |
| explicit DebugModeController(net::IPEndPoint ip_endpoint) |
| : ip_endpoint_(std::move(ip_endpoint)), |
| connector_binding_(base::ComponentContextForProcess()->outgoing().get(), |
| this) { |
| // Immediately start the service. |
| StartRemoteDebuggingServer( |
| base::BindOnce(&DebugModeController::OnDevToolsPortChanged, |
| base::Unretained(this)), |
| ip_endpoint_); |
| } |
| |
| virtual void OnDevToolsPortChanged(uint16_t port) { |
| devtools_port_ = port; |
| MaybeSendRemoteDebuggingCallbacks(); |
| } |
| |
| // Currently active DevTools port. Set to 0 on service startup error. |
| absl::optional<uint16_t> devtools_port_; |
| |
| private: |
| // chromium::internal::DevToolsConnector implementation. |
| void ConnectPerContextListener( |
| fuchsia::web::DevToolsPerContextListenerHandle listener_handle) override { |
| fuchsia::web::DevToolsPerContextListenerPtr listener; |
| listener.Bind(std::move(listener_handle)); |
| if (frame_loaded_ && devtools_port_) |
| listener->OnHttpPortOpen(devtools_port_.value()); |
| devtools_listeners_.AddInterfacePtr(std::move(listener)); |
| } |
| |
| void MaybeSendRemoteDebuggingCallbacks() { |
| if (!frame_loaded_ || !devtools_port_) |
| return; |
| |
| // If |devtools_port_| is valid then notify all listeners, otherwise |
| // disconnect them. |
| if (devtools_port_.value() == 0) { |
| devtools_listeners_.CloseAll(); |
| } else { |
| for (const auto& listener : devtools_listeners_.ptrs()) { |
| listener->get()->OnHttpPortOpen(devtools_port_.value()); |
| } |
| } |
| } |
| |
| const net::IPEndPoint ip_endpoint_; |
| |
| bool frame_loaded_ = false; |
| |
| fidl::InterfacePtrSet<fuchsia::web::DevToolsPerContextListener> |
| devtools_listeners_; |
| |
| const base::ScopedServiceBinding<chromium::internal::DevToolsConnector> |
| connector_binding_; |
| }; |
| |
| // "Mixed-mode" is used when both user and debug remote debugging are active at |
| // the same time. The service is enabled for the lifespan of the web_instance. |
| class MixedModeController : public DebugModeController { |
| public: |
| explicit MixedModeController(uint16_t server_port) |
| : DebugModeController( |
| net::IPEndPoint(net::IPAddress::IPv6AllZeros(), server_port)) {} |
| ~MixedModeController() override = default; |
| |
| // WebEngineDevToolsController overrides: |
| bool OnFrameCreated(content::WebContents* contents, |
| bool user_debugging) override { |
| return true; |
| } |
| void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override { |
| get_port_callbacks_.emplace_back(std::move(callback)); |
| MaybeNotifyGetPortCallbacks(); |
| } |
| |
| // DebugModeController overrides: |
| void OnDevToolsPortChanged(uint16_t port) override { |
| DebugModeController::OnDevToolsPortChanged(port); |
| MaybeNotifyGetPortCallbacks(); |
| } |
| |
| void MaybeNotifyGetPortCallbacks() { |
| if (!devtools_port_) |
| return; |
| for (auto& callback : get_port_callbacks_) |
| std::move(callback).Run(devtools_port_.value()); |
| get_port_callbacks_.clear(); |
| } |
| |
| std::vector<base::OnceCallback<void(uint16_t)>> get_port_callbacks_; |
| }; |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<WebEngineDevToolsController> |
| WebEngineDevToolsController::CreateFromCommandLine( |
| const base::CommandLine& command_line) { |
| absl::optional<uint16_t> devtools_port; |
| if (command_line.HasSwitch(switches::kRemoteDebuggingPort)) { |
| // Set up DevTools to listen on all network routes on the command-line |
| // provided port. |
| std::string command_line_port_value = |
| command_line.GetSwitchValueASCII(switches::kRemoteDebuggingPort); |
| int parsed_port = 0; |
| |
| // The command-line option can only be provided by the ContextProvider |
| // process, it should not fail to parse to an int. |
| bool parsed = base::StringToInt(command_line_port_value, &parsed_port); |
| DCHECK(parsed); |
| |
| if (parsed_port != 0 && |
| (!net::IsPortValid(parsed_port) || net::IsWellKnownPort(parsed_port))) { |
| LOG(WARNING) << "Invalid HTTP debugger service port number " |
| << command_line_port_value; |
| } else { |
| devtools_port = parsed_port; |
| } |
| } |
| |
| bool enable_debug_mode = |
| command_line.HasSwitch(switches::kEnableRemoteDebugMode); |
| if (devtools_port) { |
| if (enable_debug_mode) { |
| return std::make_unique<MixedModeController>(devtools_port.value()); |
| } else { |
| return std::make_unique<UserModeController>(devtools_port.value()); |
| } |
| } else if (enable_debug_mode) { |
| return std::make_unique<DebugModeController>(); |
| } else { |
| return std::make_unique<NoopController>(); |
| } |
| } |