|  | // Copyright 2014 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "chrome/browser/devtools/chrome_devtools_manager_delegate.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/command_line.h" | 
|  | #include "base/containers/contains.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/strings/strcat.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/browser/browser_features.h" | 
|  | #include "chrome/browser/devtools/chrome_devtools_session.h" | 
|  | #include "chrome/browser/devtools/device/android_device_manager.h" | 
|  | #include "chrome/browser/devtools/device/tcp_device_provider.h" | 
|  | #include "chrome/browser/devtools/devtools_availability_checker.h" | 
|  | #include "chrome/browser/devtools/devtools_browser_context_manager.h" | 
|  | #include "chrome/browser/devtools/devtools_window.h" | 
|  | #include "chrome/browser/devtools/protocol/target_handler.h" | 
|  | #include "chrome/browser/extensions/extension_tab_util.h" | 
|  | #include "chrome/browser/lifetime/application_lifetime.h" | 
|  | #include "chrome/browser/policy/developer_tools_policy_handler.h" | 
|  | #include "chrome/browser/policy/profile_policy_connector.h" | 
|  | #include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/browser/profiles/profile_manager.h" | 
|  | #include "chrome/browser/ui/browser_navigator.h" | 
|  | #include "chrome/browser/ui/browser_navigator_params.h" | 
|  | #include "chrome/browser/ui/webui_browser/webui_browser.h" | 
|  | #include "chrome/browser/web_applications/proto/web_app_install_state.pb.h" | 
|  | #include "chrome/browser/web_applications/web_app.h" | 
|  | #include "chrome/browser/web_applications/web_app_provider.h" | 
|  | #include "chrome/browser/web_applications/web_app_registrar.h" | 
|  | #include "chrome/browser/web_applications/web_app_tab_helper.h" | 
|  | #include "chrome/browser/web_applications/web_app_utils.h" | 
|  | #include "chrome/common/chrome_switches.h" | 
|  | #include "chrome/grit/browser_resources.h" | 
|  | #include "components/guest_view/browser/guest_view_base.h" | 
|  | #include "components/keep_alive_registry/keep_alive_types.h" | 
|  | #include "components/keep_alive_registry/scoped_keep_alive.h" | 
|  | #include "components/tabs/public/tab_interface.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/devtools_agent_host.h" | 
|  | #include "content/public/browser/devtools_agent_host_client_channel.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "content/public/browser/render_process_host.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/common/content_switches.h" | 
|  | #include "extensions/browser/extension_host.h" | 
|  | #include "extensions/browser/extension_registry.h" | 
|  | #include "extensions/browser/process_manager.h" | 
|  | #include "extensions/browser/view_type_utils.h" | 
|  | #include "extensions/common/manifest.h" | 
|  | #include "extensions/common/mojom/view_type.mojom.h" | 
|  | #include "ui/base/resource/resource_bundle.h" | 
|  | #include "ui/gfx/switches.h" | 
|  | #include "ui/views/controls/webview/webview.h" | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | #include "ash/constants/ash_switches.h" | 
|  | #include "chromeos/constants/chromeos_features.h" | 
|  | #endif | 
|  |  | 
|  | using content::DevToolsAgentHost; | 
|  |  | 
|  | const char ChromeDevToolsManagerDelegate::kTypeApp[] = "app"; | 
|  | const char ChromeDevToolsManagerDelegate::kTypeBackgroundPage[] = | 
|  | "background_page"; | 
|  | const char ChromeDevToolsManagerDelegate::kTypePage[] = "page"; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | std::optional<std::string> GetIsolatedWebAppNameAndVersion( | 
|  | content::WebContents* web_contents) { | 
|  | const webapps::AppId* app_id = | 
|  | web_app::WebAppTabHelper::GetAppId(web_contents); | 
|  | if (!app_id) { | 
|  | return std::nullopt; | 
|  | } | 
|  | const web_app::WebAppProvider* provider = | 
|  | web_app::WebAppProvider::GetForWebContents(web_contents); | 
|  | if (!provider) { | 
|  | return std::nullopt; | 
|  | } | 
|  | // In this case we will not modify any data and reading stale data is | 
|  | // fine, since the app will already be installed and open in the case | 
|  | // it needs to be checked in DevTools. | 
|  | const web_app::WebAppRegistrar& registrar = provider->registrar_unsafe(); | 
|  | const web_app::WebApp* web_app = registrar.GetAppById(*app_id); | 
|  |  | 
|  | if (web_app && registrar.IsIsolated(*app_id)) { | 
|  | // Version is a key part of IWA so should be displayed in inspect tool | 
|  | return base::StrCat({registrar.GetAppShortName(*app_id), " (", | 
|  | web_app->isolation_data()->version().GetString(), | 
|  | ")"}); | 
|  | } | 
|  |  | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | bool IsIsolatedWebApp(content::WebContents* web_contents) { | 
|  | return GetIsolatedWebAppNameAndVersion(web_contents).has_value(); | 
|  | } | 
|  |  | 
|  | bool GetExtensionInfo(content::WebContents* wc, | 
|  | std::string* name, | 
|  | std::string* type) { | 
|  | auto* process_manager = | 
|  | extensions::ProcessManager::Get(wc->GetBrowserContext()); | 
|  | if (!process_manager) { | 
|  | return false; | 
|  | } | 
|  | const extensions::Extension* extension = | 
|  | process_manager->GetExtensionForWebContents(wc); | 
|  | if (!extension) | 
|  | return false; | 
|  | extensions::ExtensionHost* extension_host = | 
|  | process_manager->GetBackgroundHostForExtension(extension->id()); | 
|  | if (extension_host && extension_host->host_contents() == wc) { | 
|  | *name = extension->name(); | 
|  | *type = ChromeDevToolsManagerDelegate::kTypeBackgroundPage; | 
|  | return true; | 
|  | } | 
|  | if (extension->is_hosted_app() || extension->is_legacy_packaged_app() || | 
|  | extension->is_platform_app()) { | 
|  | *name = extension->name(); | 
|  | *type = ChromeDevToolsManagerDelegate::kTypeApp; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | auto view_type = extensions::GetViewType(wc); | 
|  | if (view_type == extensions::mojom::ViewType::kExtensionPopup || | 
|  | view_type == extensions::mojom::ViewType::kExtensionSidePanel) { | 
|  | // Note that we are intentionally not setting name here, so that we can | 
|  | // construct a name based on the URL or page title in | 
|  | // RenderFrameDevToolsAgentHost::GetTitle() | 
|  | *type = ChromeDevToolsManagerDelegate::kTypePage; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (view_type == extensions::mojom::ViewType::kOffscreenDocument) { | 
|  | // Note that we are intentionally not setting name here, so that we can | 
|  | // construct a name based on the URL or page title in | 
|  | // RenderFrameDevToolsAgentHost::GetTitle() | 
|  | // | 
|  | // Use `kTypeBackgroundPage` for offscreen doc until devtools frontend is | 
|  | // updated to support a new `offscreen_document` target type. Otherwise, | 
|  | // DOM is not inspectable. | 
|  | *type = ChromeDevToolsManagerDelegate::kTypeBackgroundPage; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Set type to other for extensions if not matched previously. | 
|  | *type = DevToolsAgentHost::kTypeOther; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | ChromeDevToolsManagerDelegate* g_instance; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // static | 
|  | ChromeDevToolsManagerDelegate* ChromeDevToolsManagerDelegate::GetInstance() { | 
|  | return g_instance; | 
|  | } | 
|  |  | 
|  | ChromeDevToolsManagerDelegate::ChromeDevToolsManagerDelegate() { | 
|  | DCHECK(!g_instance); | 
|  | g_instance = this; | 
|  |  | 
|  | #if !BUILDFLAG(IS_CHROMEOS) | 
|  | // Only create and hold keep alive for automation test for non ChromeOS. | 
|  | // ChromeOS automation test (aka tast) manages chrome instance via session | 
|  | // manager daemon. The extra keep alive is not needed and makes ChromeOS | 
|  | // not able to shutdown chrome properly. See https://crbug.com/1174627. | 
|  | base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); | 
|  | if ((command_line->HasSwitch(switches::kNoStartupWindow) || | 
|  | command_line->HasSwitch(switches::kHeadless)) && | 
|  | (command_line->HasSwitch(switches::kRemoteDebuggingPipe) || | 
|  | command_line->HasSwitch(switches::kRemoteDebuggingPort))) { | 
|  | // If running without a startup window with remote debugging, | 
|  | // we are controlled entirely by the automation process. | 
|  | // Keep the application running until explicit close through DevTools | 
|  | // protocol. | 
|  | keep_alive_ = std::make_unique<ScopedKeepAlive>( | 
|  | KeepAliveOrigin::REMOTE_DEBUGGING, KeepAliveRestartOption::DISABLED); | 
|  |  | 
|  | // Also keep the initial profile alive so that TargetHandler::CreateTarget() | 
|  | // can retrieve it without risking disk access even when all pages are | 
|  | // closed. Keep-a-living the very first loaded profile looks like a | 
|  | // reasonable option. | 
|  | if (Profile* profile = ProfileManager::GetLastUsedProfile()) { | 
|  | if (profile->IsOffTheRecord()) { | 
|  | profile = profile->GetOriginalProfile(); | 
|  | } | 
|  | profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>( | 
|  | profile, ProfileKeepAliveOrigin::kRemoteDebugging); | 
|  | } | 
|  | } | 
|  | #endif  // !BUILDFLAG(IS_CHROMEOS) | 
|  | } | 
|  |  | 
|  | ChromeDevToolsManagerDelegate::~ChromeDevToolsManagerDelegate() { | 
|  | DCHECK(g_instance == this); | 
|  | g_instance = nullptr; | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::Inspect( | 
|  | content::DevToolsAgentHost* agent_host) { | 
|  | DevToolsWindow::OpenDevToolsWindow(agent_host, nullptr, | 
|  | DevToolsOpenedByAction::kInspectLink); | 
|  | } | 
|  |  | 
|  | scoped_refptr<content::DevToolsAgentHost> | 
|  | ChromeDevToolsManagerDelegate::OpenDevTools( | 
|  | content::DevToolsAgentHost* agent_host) { | 
|  | scoped_refptr<content::DevToolsAgentHost> tab_agent_host( | 
|  | content::DevToolsAgentHost::GetOrCreateForTab( | 
|  | agent_host->GetWebContents())); | 
|  | DevToolsWindow::OpenDevToolsWindow(tab_agent_host, nullptr, | 
|  | DevToolsOpenedByAction::kUnknown); | 
|  | DevToolsWindow* window = | 
|  | DevToolsWindow::FindDevToolsWindow(tab_agent_host.get()); | 
|  | if (!window) { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | return DevToolsAgentHost::GetOrCreateFor(window->GetDevToolsWebContents()); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::Activate( | 
|  | content::DevToolsAgentHost* agent_host) { | 
|  | auto* web_contents = agent_host->GetWebContents(); | 
|  | if (!web_contents) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Brings the tab to foreground. We need to do this in case the devtools | 
|  | // window is undocked and this is being called from another tab that is in | 
|  | // the foreground. | 
|  | web_contents->GetDelegate()->ActivateContents(web_contents); | 
|  |  | 
|  | // Brings a undocked devtools window to the foreground. | 
|  | DevToolsWindow* devtools_window = | 
|  | DevToolsWindow::GetInstanceForInspectedWebContents( | 
|  | agent_host->GetWebContents()); | 
|  | if (!devtools_window) { | 
|  | return; | 
|  | } | 
|  | devtools_window->ActivateWindow(); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::HandleCommand( | 
|  | content::DevToolsAgentHostClientChannel* channel, | 
|  | base::span<const uint8_t> message, | 
|  | NotHandledCallback callback) { | 
|  | auto it = sessions_.find(channel); | 
|  | if (it == sessions_.end()) { | 
|  | std::move(callback).Run(message); | 
|  | // This should not happen, but happens. NOTREACHED tries to get | 
|  | // a repro in some test. | 
|  | NOTREACHED(); | 
|  | } | 
|  | it->second->HandleCommand(message, std::move(callback)); | 
|  | } | 
|  |  | 
|  | std::string ChromeDevToolsManagerDelegate::GetTargetType( | 
|  | content::WebContents* web_contents) { | 
|  | if (webui_browser::IsBrowserUIWebContents(web_contents)) { | 
|  | return DevToolsAgentHost::kTypeBrowserUI; | 
|  | } | 
|  |  | 
|  | if (IsIsolatedWebApp(web_contents)) { | 
|  | return ChromeDevToolsManagerDelegate::kTypeApp; | 
|  | } | 
|  |  | 
|  | if (tabs::TabInterface::MaybeGetFromContents(web_contents)) { | 
|  | return DevToolsAgentHost::kTypePage; | 
|  | } | 
|  |  | 
|  | std::string extension_name; | 
|  | std::string extension_type; | 
|  | if (GetExtensionInfo(web_contents, &extension_name, &extension_type)) { | 
|  | return extension_type; | 
|  | } | 
|  |  | 
|  | if (views::WebView::IsWebViewContents(web_contents)) { | 
|  | return DevToolsAgentHost::kTypePage; | 
|  | } | 
|  |  | 
|  | return DevToolsAgentHost::kTypeOther; | 
|  | } | 
|  |  | 
|  | std::optional<bool> ChromeDevToolsManagerDelegate::ShouldReportAsTabTarget( | 
|  | content::WebContents* web_contents) { | 
|  | if (webui_browser::IsBrowserUIWebContents(web_contents)) { | 
|  | // Return false for browser UI so its WebContents is not reported as Tab. | 
|  | // Browser UI is not a Tab and can not be interacted as a Tab. Reporting | 
|  | // browser UI WebContents as Tab target will confuse tools such as test | 
|  | // drivers and cause them to fail. | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (tabs::TabInterface::MaybeGetFromContents(web_contents)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | std::string ChromeDevToolsManagerDelegate::GetTargetTitle( | 
|  | content::WebContents* web_contents) { | 
|  | if (auto iwa_name_version = GetIsolatedWebAppNameAndVersion(web_contents)) { | 
|  | return *iwa_name_version; | 
|  | } | 
|  |  | 
|  | std::string extension_name; | 
|  | std::string extension_type; | 
|  | if (!GetExtensionInfo(web_contents, &extension_name, &extension_type)) { | 
|  | return std::string(); | 
|  | } | 
|  |  | 
|  | return extension_name; | 
|  | } | 
|  |  | 
|  | bool ChromeDevToolsManagerDelegate::AllowInspectingRenderFrameHost( | 
|  | content::RenderFrameHost* rfh) { | 
|  | Profile* profile = | 
|  | Profile::FromBrowserContext(rfh->GetProcess()->GetBrowserContext()); | 
|  | return IsInspectionAllowed(profile, | 
|  | content::WebContents::FromRenderFrameHost(rfh)); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::ClientAttached( | 
|  | content::DevToolsAgentHostClientChannel* channel) { | 
|  | DCHECK(sessions_.find(channel) == sessions_.end()); | 
|  | sessions_.emplace(channel, std::make_unique<ChromeDevToolsSession>(channel)); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::ClientDetached( | 
|  | content::DevToolsAgentHostClientChannel* channel) { | 
|  | sessions_.erase(channel); | 
|  | } | 
|  |  | 
|  | scoped_refptr<DevToolsAgentHost> ChromeDevToolsManagerDelegate::CreateNewTarget( | 
|  | const GURL& url, | 
|  | DevToolsManagerDelegate::TargetType target_type, | 
|  | bool new_window) { | 
|  | NavigateParams params(ProfileManager::GetLastUsedProfile(), url, | 
|  | ui::PAGE_TRANSITION_AUTO_TOPLEVEL); | 
|  | params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; | 
|  | Navigate(¶ms); | 
|  | if (!params.navigated_or_inserted_contents) | 
|  | return nullptr; | 
|  | return target_type == DevToolsManagerDelegate::kTab | 
|  | ? DevToolsAgentHost::GetOrCreateForTab( | 
|  | params.navigated_or_inserted_contents) | 
|  | : DevToolsAgentHost::GetOrCreateFor( | 
|  | params.navigated_or_inserted_contents); | 
|  | } | 
|  |  | 
|  | std::vector<content::BrowserContext*> | 
|  | ChromeDevToolsManagerDelegate::GetBrowserContexts() { | 
|  | return DevToolsBrowserContextManager::GetInstance().GetBrowserContexts(); | 
|  | } | 
|  |  | 
|  | content::BrowserContext* | 
|  | ChromeDevToolsManagerDelegate::GetDefaultBrowserContext() { | 
|  | return DevToolsBrowserContextManager::GetInstance() | 
|  | .GetDefaultBrowserContext(); | 
|  | } | 
|  |  | 
|  | content::BrowserContext* ChromeDevToolsManagerDelegate::CreateBrowserContext() { | 
|  | return DevToolsBrowserContextManager::GetInstance().CreateBrowserContext(); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::DisposeBrowserContext( | 
|  | content::BrowserContext* context, | 
|  | DisposeCallback callback) { | 
|  | DevToolsBrowserContextManager::GetInstance().DisposeBrowserContext( | 
|  | context, std::move(callback)); | 
|  | } | 
|  |  | 
|  | bool ChromeDevToolsManagerDelegate::HasBundledFrontendResources() { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::DevicesAvailable( | 
|  | const DevToolsDeviceDiscovery::CompleteDevices& devices) { | 
|  | DevToolsAgentHost::List remote_targets; | 
|  | for (const auto& complete : devices) { | 
|  | for (const auto& browser : complete.second->browsers()) { | 
|  | for (const auto& page : browser->pages()) | 
|  | remote_targets.push_back(page->CreateTarget()); | 
|  | } | 
|  | } | 
|  | remote_agent_hosts_.swap(remote_targets); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::UpdateDeviceDiscovery() { | 
|  | RemoteLocations remote_locations; | 
|  | for (const auto& it : sessions_) { | 
|  | TargetHandler* target_handler = it.second->target_handler(); | 
|  | if (!target_handler) | 
|  | continue; | 
|  | RemoteLocations& locations = target_handler->remote_locations(); | 
|  | remote_locations.insert(locations.begin(), locations.end()); | 
|  | } | 
|  |  | 
|  | if (remote_locations == remote_locations_) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (remote_locations.empty()) { | 
|  | device_discovery_.reset(); | 
|  | remote_agent_hosts_.clear(); | 
|  | } else { | 
|  | if (!device_manager_) | 
|  | device_manager_ = AndroidDeviceManager::Create(); | 
|  |  | 
|  | AndroidDeviceManager::DeviceProviders providers; | 
|  | providers.push_back(new TCPDeviceProvider(remote_locations)); | 
|  | device_manager_->SetDeviceProviders(providers); | 
|  |  | 
|  | device_discovery_ = std::make_unique<DevToolsDeviceDiscovery>( | 
|  | device_manager_.get(), | 
|  | base::BindRepeating(&ChromeDevToolsManagerDelegate::DevicesAvailable, | 
|  | base::Unretained(this))); | 
|  | } | 
|  | remote_locations_.swap(remote_locations); | 
|  | } | 
|  |  | 
|  | void ChromeDevToolsManagerDelegate::ResetAndroidDeviceManagerForTesting() { | 
|  | device_manager_.reset(); | 
|  |  | 
|  | // We also need |device_discovery_| to go away because there may be a pending | 
|  | // task using a raw pointer to the DeviceManager we just deleted. | 
|  | device_discovery_.reset(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ChromeDevToolsManagerDelegate::CloseBrowserSoon() { | 
|  | content::GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, base::BindOnce([]() { | 
|  | // Do not keep the application running anymore, we got an explicit | 
|  | // request to close. | 
|  | AllowBrowserToClose(); | 
|  | chrome::ExitIgnoreUnloadHandlers(); | 
|  | })); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ChromeDevToolsManagerDelegate::AllowBrowserToClose() { | 
|  | if (auto* instance = GetInstance()) { | 
|  | instance->keep_alive_.reset(); | 
|  | } | 
|  | } |