| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include <memory> | 5 #include <memory> |
| 6 #include <sstream> | 6 #include <sstream> |
| 7 #include <string> | 7 #include <string> |
| 8 | 8 |
| 9 #include "base/base64.h" | 9 #include "base/base64.h" |
| 10 #include "base/bind.h" | 10 #include "base/bind.h" |
| 11 #include "base/callback.h" | 11 #include "base/callback.h" |
| 12 #include "base/command_line.h" | 12 #include "base/command_line.h" |
| 13 #include "base/files/file_path.h" | 13 #include "base/files/file_path.h" |
| 14 #include "base/json/json_writer.h" | 14 #include "base/json/json_writer.h" |
| 15 #include "base/location.h" | 15 #include "base/location.h" |
| 16 #include "base/memory/ptr_util.h" | |
| 17 #include "base/memory/ref_counted.h" | |
| 18 #include "base/memory/weak_ptr.h" | 16 #include "base/memory/weak_ptr.h" |
| 19 #include "base/numerics/safe_conversions.h" | 17 #include "base/numerics/safe_conversions.h" |
| 20 #include "base/strings/string_number_conversions.h" | 18 #include "base/strings/string_number_conversions.h" |
| 19 #include "headless/app/headless_shell.h" |
| 21 #include "headless/app/headless_shell_switches.h" | 20 #include "headless/app/headless_shell_switches.h" |
| 22 #include "headless/public/devtools/domains/emulation.h" | |
| 23 #include "headless/public/devtools/domains/inspector.h" | |
| 24 #include "headless/public/devtools/domains/page.h" | |
| 25 #include "headless/public/devtools/domains/runtime.h" | |
| 26 #include "headless/public/headless_browser.h" | |
| 27 #include "headless/public/headless_devtools_client.h" | |
| 28 #include "headless/public/headless_devtools_target.h" | 21 #include "headless/public/headless_devtools_target.h" |
| 29 #include "headless/public/headless_web_contents.h" | |
| 30 #include "headless/public/util/deterministic_dispatcher.h" | |
| 31 #include "headless/public/util/deterministic_http_protocol_handler.h" | 22 #include "headless/public/util/deterministic_http_protocol_handler.h" |
| 32 #include "net/base/file_stream.h" | |
| 33 #include "net/base/io_buffer.h" | 23 #include "net/base/io_buffer.h" |
| 34 #include "net/base/ip_address.h" | 24 #include "net/base/ip_address.h" |
| 35 #include "net/base/net_errors.h" | 25 #include "net/base/net_errors.h" |
| 36 #include "ui/gfx/geometry/size.h" | 26 #include "ui/gfx/geometry/size.h" |
| 37 | 27 |
| 38 namespace headless { | 28 namespace headless { |
| 39 namespace { | 29 namespace { |
| 40 // Address where to listen to incoming DevTools connections. | 30 // Address where to listen to incoming DevTools connections. |
| 41 const char kDevToolsHttpServerAddress[] = "127.0.0.1"; | 31 const char kDevToolsHttpServerAddress[] = "127.0.0.1"; |
| 42 // Default file name for screenshot. Can be overriden by "--screenshot" switch. | 32 // Default file name for screenshot. Can be overriden by "--screenshot" switch. |
| 43 const char kDefaultScreenshotFileName[] = "screenshot.png"; | 33 const char kDefaultScreenshotFileName[] = "screenshot.png"; |
| 44 | 34 |
| 45 bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { | 35 bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { |
| 46 int width, height = 0; | 36 int width, height = 0; |
| 47 if (sscanf(window_size.c_str(), "%d%*[x,]%d", &width, &height) >= 2 && | 37 if (sscanf(window_size.c_str(), "%d%*[x,]%d", &width, &height) >= 2 && |
| 48 width >= 0 && height >= 0) { | 38 width >= 0 && height >= 0) { |
| 49 parsed_window_size->set_width(width); | 39 parsed_window_size->set_width(width); |
| 50 parsed_window_size->set_height(height); | 40 parsed_window_size->set_height(height); |
| 51 return true; | 41 return true; |
| 52 } | 42 } |
| 53 return false; | 43 return false; |
| 54 } | 44 } |
| 55 } // namespace | 45 } // namespace |
| 56 | 46 |
| 57 // An application which implements a simple headless browser. | 47 HeadlessShell::HeadlessShell() |
| 58 class HeadlessShell : public HeadlessWebContents::Observer, | 48 : browser_(nullptr), |
| 59 emulation::ExperimentalObserver, | 49 devtools_client_(HeadlessDevToolsClient::Create()), |
| 60 inspector::ExperimentalObserver, | 50 web_contents_(nullptr), |
| 61 page::Observer { | 51 processed_page_ready_(false), |
| 62 public: | 52 browser_context_(nullptr), |
| 63 HeadlessShell() | 53 weak_factory_(this) {} |
| 64 : browser_(nullptr), | 54 |
| 65 devtools_client_(HeadlessDevToolsClient::Create()), | 55 HeadlessShell::~HeadlessShell() {} |
| 66 web_contents_(nullptr), | 56 |
| 67 processed_page_ready_(false), | 57 void HeadlessShell::OnStart(HeadlessBrowser* browser) { |
| 68 browser_context_(nullptr), | 58 browser_ = browser; |
| 69 weak_factory_(this) {} | 59 |
| 70 ~HeadlessShell() override {} | 60 HeadlessBrowserContext::Builder context_builder = |
| 71 | 61 browser_->CreateBrowserContextBuilder(); |
| 72 void OnStart(HeadlessBrowser* browser) { | 62 // TODO(eseckler): These switches should also affect BrowserContexts that |
| 73 browser_ = browser; | 63 // are created via DevTools later. |
| 74 | 64 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 75 HeadlessBrowserContext::Builder context_builder = | 65 switches::kDeterministicFetch)) { |
| 76 browser_->CreateBrowserContextBuilder(); | 66 deterministic_dispatcher_.reset( |
| 77 // TODO(eseckler): These switches should also affect BrowserContexts that | 67 new DeterministicDispatcher(browser_->BrowserIOThread())); |
| 78 // are created via DevTools later. | 68 |
| 79 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 69 ProtocolHandlerMap protocol_handlers; |
| 80 switches::kDeterministicFetch)) { | 70 protocol_handlers[url::kHttpScheme] = |
| 81 deterministic_dispatcher_.reset( | 71 base::MakeUnique<DeterministicHttpProtocolHandler>( |
| 82 new DeterministicDispatcher(browser_->BrowserIOThread())); | 72 deterministic_dispatcher_.get(), browser->BrowserIOThread()); |
| 83 | 73 protocol_handlers[url::kHttpsScheme] = |
| 84 ProtocolHandlerMap protocol_handlers; | 74 base::MakeUnique<DeterministicHttpProtocolHandler>( |
| 85 protocol_handlers[url::kHttpScheme] = | 75 deterministic_dispatcher_.get(), browser->BrowserIOThread()); |
| 86 base::MakeUnique<DeterministicHttpProtocolHandler>( | 76 |
| 87 deterministic_dispatcher_.get(), browser->BrowserIOThread()); | 77 context_builder.SetProtocolHandlers(std::move(protocol_handlers)); |
| 88 protocol_handlers[url::kHttpsScheme] = | 78 } |
| 89 base::MakeUnique<DeterministicHttpProtocolHandler>( | 79 browser_context_ = context_builder.Build(); |
| 90 deterministic_dispatcher_.get(), browser->BrowserIOThread()); | 80 browser_->SetDefaultBrowserContext(browser_context_); |
| 91 | 81 |
| 92 context_builder.SetProtocolHandlers(std::move(protocol_handlers)); | 82 HeadlessWebContents::Builder builder( |
| 93 } | 83 browser_context_->CreateWebContentsBuilder()); |
| 94 browser_context_ = context_builder.Build(); | 84 base::CommandLine::StringVector args = |
| 95 browser_->SetDefaultBrowserContext(browser_context_); | 85 base::CommandLine::ForCurrentProcess()->GetArgs(); |
| 96 | 86 |
| 97 HeadlessWebContents::Builder builder( | 87 // TODO(alexclarke): Should we navigate to about:blank first if using |
| 98 browser_context_->CreateWebContentsBuilder()); | 88 // virtual time? |
| 99 base::CommandLine::StringVector args = | 89 if (args.empty()) |
| 100 base::CommandLine::ForCurrentProcess()->GetArgs(); | 90 args.push_back("about:blank"); |
| 101 | 91 for (auto it = args.rbegin(); it != args.rend(); ++it) { |
| 102 // TODO(alexclarke): Should we navigate to about:blank first if using | 92 GURL url(*it); |
| 103 // virtual time? | 93 HeadlessWebContents* web_contents = builder.SetInitialURL(url).Build(); |
| 104 if (args.empty()) | 94 if (!web_contents) { |
| 105 args.push_back("about:blank"); | 95 LOG(ERROR) << "Navigation to " << url << " failed"; |
| 106 for (auto it = args.rbegin(); it != args.rend(); ++it) { | 96 browser_->Shutdown(); |
| 107 GURL url(*it); | |
| 108 HeadlessWebContents* web_contents = builder.SetInitialURL(url).Build(); | |
| 109 if (!web_contents) { | |
| 110 LOG(ERROR) << "Navigation to " << url << " failed"; | |
| 111 browser_->Shutdown(); | |
| 112 return; | |
| 113 } | |
| 114 if (!web_contents_ && !RemoteDebuggingEnabled()) { | |
| 115 // TODO(jzfeng): Support observing multiple targets. | |
| 116 url_ = url; | |
| 117 web_contents_ = web_contents; | |
| 118 web_contents_->AddObserver(this); | |
| 119 } | |
| 120 } | |
| 121 } | |
| 122 | |
| 123 void Shutdown() { | |
| 124 if (!web_contents_) | |
| 125 return; | 97 return; |
| 126 if (!RemoteDebuggingEnabled()) { | 98 } |
| 127 devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this); | 99 if (!web_contents_ && !RemoteDebuggingEnabled()) { |
| 128 devtools_client_->GetInspector()->GetExperimental()->RemoveObserver(this); | 100 // TODO(jzfeng): Support observing multiple targets. |
| 129 devtools_client_->GetPage()->RemoveObserver(this); | 101 url_ = url; |
| 130 if (web_contents_->GetDevToolsTarget()) { | 102 web_contents_ = web_contents; |
| 131 web_contents_->GetDevToolsTarget()->DetachClient( | 103 web_contents_->AddObserver(this); |
| 132 devtools_client_.get()); | 104 } |
| 133 } | 105 } |
| 134 } | 106 } |
| 135 web_contents_->RemoveObserver(this); | 107 |
| 136 web_contents_ = nullptr; | 108 void HeadlessShell::Shutdown() { |
| 137 browser_context_->Close(); | 109 if (!web_contents_) |
| 138 browser_->Shutdown(); | 110 return; |
| 139 } | 111 if (!RemoteDebuggingEnabled()) { |
| 140 | 112 devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this); |
| 141 // HeadlessWebContents::Observer implementation: | 113 devtools_client_->GetInspector()->GetExperimental()->RemoveObserver(this); |
| 142 void DevToolsTargetReady() override { | 114 devtools_client_->GetPage()->GetExperimental()->RemoveObserver(this); |
| 143 web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); | 115 if (web_contents_->GetDevToolsTarget()) { |
| 144 devtools_client_->GetInspector()->GetExperimental()->AddObserver(this); | 116 web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); |
| 145 devtools_client_->GetPage()->AddObserver(this); | 117 } |
| 146 devtools_client_->GetPage()->Enable(); | 118 } |
| 147 // Check if the document had already finished loading by the time we | 119 web_contents_->RemoveObserver(this); |
| 148 // attached. | 120 web_contents_ = nullptr; |
| 149 | 121 browser_context_->Close(); |
| 150 devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); | 122 browser_->Shutdown(); |
| 151 | 123 } |
| 152 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 124 |
| 153 switches::kVirtualTimeBudget)) { | 125 void HeadlessShell::DevToolsTargetReady() { |
| 154 std::string budget_ms_ascii = | 126 web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); |
| 155 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | 127 devtools_client_->GetInspector()->GetExperimental()->AddObserver(this); |
| 156 switches::kVirtualTimeBudget); | 128 devtools_client_->GetPage()->GetExperimental()->AddObserver(this); |
| 157 int budget_ms; | 129 devtools_client_->GetPage()->Enable(); |
| 158 CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) | 130 // Check if the document had already finished loading by the time we |
| 159 << "Expected an integer value for --virtual-time-budget="; | 131 // attached. |
| 160 devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( | 132 |
| 161 emulation::SetVirtualTimePolicyParams::Builder() | 133 devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); |
| 162 .SetPolicy(emulation::VirtualTimePolicy:: | 134 |
| 163 PAUSE_IF_NETWORK_FETCHES_PENDING) | 135 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 164 .SetBudget(budget_ms) | 136 switches::kDeterministicFetch)) { |
| 165 .Build()); | 137 devtools_client_->GetPage()->GetExperimental()->SetControlNavigations( |
| 166 } else { | 138 headless::page::SetControlNavigationsParams::Builder() |
| 167 PollReadyState(); | 139 .SetEnabled(true) |
| 168 } | 140 .Build()); |
| 169 | 141 } |
| 170 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTimeout)) { | 142 |
| 171 std::string timeout_ms_ascii = | 143 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 172 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | 144 switches::kVirtualTimeBudget)) { |
| 173 switches::kTimeout); | 145 std::string budget_ms_ascii = |
| 174 int timeout_ms; | 146 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| 175 CHECK(base::StringToInt(timeout_ms_ascii, &timeout_ms)) | 147 switches::kVirtualTimeBudget); |
| 176 << "Expected an integer value for --timeout="; | 148 int budget_ms; |
| 177 browser_->BrowserMainThread()->PostDelayedTask( | 149 CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) |
| 178 FROM_HERE, | 150 << "Expected an integer value for --virtual-time-budget="; |
| 179 base::Bind(&HeadlessShell::FetchTimeout, weak_factory_.GetWeakPtr()), | 151 devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( |
| 180 base::TimeDelta::FromMilliseconds(timeout_ms)); | 152 emulation::SetVirtualTimePolicyParams::Builder() |
| 181 } | 153 .SetPolicy( |
| 182 | 154 emulation::VirtualTimePolicy::PAUSE_IF_NETWORK_FETCHES_PENDING) |
| 183 // TODO(skyostil): Implement more features to demonstrate the devtools API. | 155 .SetBudget(budget_ms) |
| 184 } | 156 .Build()); |
| 185 | 157 } else { |
| 186 void FetchTimeout() { | 158 PollReadyState(); |
| 187 LOG(INFO) << "Timeout."; | 159 } |
| 188 devtools_client_->GetPage()->GetExperimental()->StopLoading( | 160 |
| 189 page::StopLoadingParams::Builder().Build()); | 161 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTimeout)) { |
| 190 } | 162 std::string timeout_ms_ascii = |
| 191 | 163 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| 192 void OnTargetCrashed(const inspector::TargetCrashedParams& params) override { | 164 switches::kTimeout); |
| 193 LOG(ERROR) << "Abnormal renderer termination."; | 165 int timeout_ms; |
| 194 // NB this never gets called if remote debugging is enabled. | 166 CHECK(base::StringToInt(timeout_ms_ascii, &timeout_ms)) |
| 167 << "Expected an integer value for --timeout="; |
| 168 browser_->BrowserMainThread()->PostDelayedTask( |
| 169 FROM_HERE, |
| 170 base::Bind(&HeadlessShell::FetchTimeout, weak_factory_.GetWeakPtr()), |
| 171 base::TimeDelta::FromMilliseconds(timeout_ms)); |
| 172 } |
| 173 |
| 174 // TODO(skyostil): Implement more features to demonstrate the devtools API. |
| 175 } |
| 176 |
| 177 void HeadlessShell::FetchTimeout() { |
| 178 LOG(INFO) << "Timeout."; |
| 179 devtools_client_->GetPage()->GetExperimental()->StopLoading( |
| 180 page::StopLoadingParams::Builder().Build()); |
| 181 } |
| 182 |
| 183 void HeadlessShell::OnTargetCrashed( |
| 184 const inspector::TargetCrashedParams& params) { |
| 185 LOG(ERROR) << "Abnormal renderer termination."; |
| 186 // NB this never gets called if remote debugging is enabled. |
| 187 Shutdown(); |
| 188 } |
| 189 |
| 190 void HeadlessShell::PollReadyState() { |
| 191 // We need to check the current location in addition to the ready state to |
| 192 // be sure the expected page is ready. |
| 193 devtools_client_->GetRuntime()->Evaluate( |
| 194 "document.readyState + ' ' + document.location.href", |
| 195 base::Bind(&HeadlessShell::OnReadyState, weak_factory_.GetWeakPtr())); |
| 196 } |
| 197 |
| 198 void HeadlessShell::OnReadyState( |
| 199 std::unique_ptr<runtime::EvaluateResult> result) { |
| 200 std::string ready_state_and_url; |
| 201 if (result->GetResult()->GetValue()->GetAsString(&ready_state_and_url)) { |
| 202 std::stringstream stream(ready_state_and_url); |
| 203 std::string ready_state; |
| 204 std::string url; |
| 205 stream >> ready_state; |
| 206 stream >> url; |
| 207 |
| 208 if (ready_state == "complete" && |
| 209 (url_.spec() == url || url != "about:blank")) { |
| 210 OnPageReady(); |
| 211 return; |
| 212 } |
| 213 } |
| 214 } |
| 215 |
| 216 // emulation::Observer implementation: |
| 217 void HeadlessShell::OnVirtualTimeBudgetExpired( |
| 218 const emulation::VirtualTimeBudgetExpiredParams& params) { |
| 219 OnPageReady(); |
| 220 } |
| 221 |
| 222 // page::Observer implementation: |
| 223 void HeadlessShell::OnLoadEventFired(const page::LoadEventFiredParams& params) { |
| 224 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 225 switches::kVirtualTimeBudget)) { |
| 226 return; |
| 227 } |
| 228 OnPageReady(); |
| 229 } |
| 230 |
| 231 void HeadlessShell::OnNavigationRequested( |
| 232 const headless::page::NavigationRequestedParams& params) { |
| 233 deterministic_dispatcher_->NavigationRequested( |
| 234 base::MakeUnique<ShellNavigationRequest>(weak_factory_.GetWeakPtr(), |
| 235 params)); |
| 236 } |
| 237 |
| 238 void HeadlessShell::OnPageReady() { |
| 239 if (processed_page_ready_) |
| 240 return; |
| 241 processed_page_ready_ = true; |
| 242 |
| 243 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDumpDom)) { |
| 244 FetchDom(); |
| 245 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 246 switches::kRepl)) { |
| 247 LOG(INFO) |
| 248 << "Type a Javascript expression to evaluate or \"quit\" to exit."; |
| 249 InputExpression(); |
| 250 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| 251 switches::kScreenshot)) { |
| 252 CaptureScreenshot(); |
| 253 } else { |
| 195 Shutdown(); | 254 Shutdown(); |
| 196 } | 255 } |
| 197 | 256 } |
| 198 void PollReadyState() { | 257 |
| 199 // We need to check the current location in addition to the ready state to | 258 void HeadlessShell::FetchDom() { |
| 200 // be sure the expected page is ready. | 259 devtools_client_->GetRuntime()->Evaluate( |
| 201 devtools_client_->GetRuntime()->Evaluate( | 260 "document.body.innerHTML", |
| 202 "document.readyState + ' ' + document.location.href", | 261 base::Bind(&HeadlessShell::OnDomFetched, weak_factory_.GetWeakPtr())); |
| 203 base::Bind(&HeadlessShell::OnReadyState, weak_factory_.GetWeakPtr())); | 262 } |
| 204 } | 263 |
| 205 | 264 void HeadlessShell::OnDomFetched( |
| 206 void OnReadyState(std::unique_ptr<runtime::EvaluateResult> result) { | 265 std::unique_ptr<runtime::EvaluateResult> result) { |
| 207 std::string ready_state_and_url; | 266 if (result->HasExceptionDetails()) { |
| 208 if (result->GetResult()->GetValue()->GetAsString(&ready_state_and_url)) { | 267 LOG(ERROR) << "Failed to evaluate document.body.innerHTML: " |
| 209 std::stringstream stream(ready_state_and_url); | 268 << result->GetExceptionDetails()->GetText(); |
| 210 std::string ready_state; | 269 } else { |
| 211 std::string url; | 270 std::string dom; |
| 212 stream >> ready_state; | 271 if (result->GetResult()->GetValue()->GetAsString(&dom)) { |
| 213 stream >> url; | 272 printf("%s\n", dom.c_str()); |
| 214 | 273 } |
| 215 if (ready_state == "complete" && | 274 } |
| 216 (url_.spec() == url || url != "about:blank")) { | 275 Shutdown(); |
| 217 OnPageReady(); | 276 } |
| 218 return; | 277 |
| 219 } | 278 void HeadlessShell::InputExpression() { |
| 220 } | 279 // Note that a real system should read user input asynchronously, because |
| 221 } | 280 // otherwise all other browser activity is suspended (e.g., page loading). |
| 222 | 281 printf(">>> "); |
| 223 // emulation::Observer implementation: | 282 std::stringstream expression; |
| 224 void OnVirtualTimeBudgetExpired( | 283 while (true) { |
| 225 const emulation::VirtualTimeBudgetExpiredParams& params) override { | 284 int c = fgetc(stdin); |
| 226 OnPageReady(); | 285 if (c == EOF || c == '\n') { |
| 227 } | 286 break; |
| 228 | 287 } |
| 229 // page::Observer implementation: | 288 expression << static_cast<char>(c); |
| 230 void OnLoadEventFired(const page::LoadEventFiredParams& params) override { | 289 } |
| 231 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 290 if (expression.str() == "quit") { |
| 232 switches::kVirtualTimeBudget)) { | |
| 233 return; | |
| 234 } | |
| 235 OnPageReady(); | |
| 236 } | |
| 237 | |
| 238 void OnPageReady() { | |
| 239 if (processed_page_ready_) | |
| 240 return; | |
| 241 processed_page_ready_ = true; | |
| 242 | |
| 243 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDumpDom)) { | |
| 244 FetchDom(); | |
| 245 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 246 switches::kRepl)) { | |
| 247 LOG(INFO) | |
| 248 << "Type a Javascript expression to evaluate or \"quit\" to exit."; | |
| 249 InputExpression(); | |
| 250 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| 251 switches::kScreenshot)) { | |
| 252 CaptureScreenshot(); | |
| 253 } else { | |
| 254 Shutdown(); | |
| 255 } | |
| 256 } | |
| 257 | |
| 258 void FetchDom() { | |
| 259 devtools_client_->GetRuntime()->Evaluate( | |
| 260 "document.body.innerHTML", | |
| 261 base::Bind(&HeadlessShell::OnDomFetched, weak_factory_.GetWeakPtr())); | |
| 262 } | |
| 263 | |
| 264 void OnDomFetched(std::unique_ptr<runtime::EvaluateResult> result) { | |
| 265 if (result->HasExceptionDetails()) { | |
| 266 LOG(ERROR) << "Failed to evaluate document.body.innerHTML: " | |
| 267 << result->GetExceptionDetails()->GetText(); | |
| 268 } else { | |
| 269 std::string dom; | |
| 270 if (result->GetResult()->GetValue()->GetAsString(&dom)) { | |
| 271 printf("%s\n", dom.c_str()); | |
| 272 } | |
| 273 } | |
| 274 Shutdown(); | 291 Shutdown(); |
| 275 } | 292 return; |
| 276 | 293 } |
| 277 void InputExpression() { | 294 devtools_client_->GetRuntime()->Evaluate( |
| 278 // Note that a real system should read user input asynchronously, because | 295 expression.str(), base::Bind(&HeadlessShell::OnExpressionResult, |
| 279 // otherwise all other browser activity is suspended (e.g., page loading). | 296 weak_factory_.GetWeakPtr())); |
| 280 printf(">>> "); | 297 } |
| 281 std::stringstream expression; | 298 |
| 282 while (true) { | 299 void HeadlessShell::OnExpressionResult( |
| 283 int c = fgetc(stdin); | 300 std::unique_ptr<runtime::EvaluateResult> result) { |
| 284 if (c == EOF || c == '\n') { | 301 std::unique_ptr<base::Value> value = result->Serialize(); |
| 285 break; | 302 std::string result_json; |
| 286 } | 303 base::JSONWriter::Write(*value, &result_json); |
| 287 expression << static_cast<char>(c); | 304 printf("%s\n", result_json.c_str()); |
| 288 } | 305 InputExpression(); |
| 289 if (expression.str() == "quit") { | 306 } |
| 290 Shutdown(); | 307 |
| 291 return; | 308 void HeadlessShell::CaptureScreenshot() { |
| 292 } | 309 devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( |
| 293 devtools_client_->GetRuntime()->Evaluate( | 310 page::CaptureScreenshotParams::Builder().Build(), |
| 294 expression.str(), base::Bind(&HeadlessShell::OnExpressionResult, | 311 base::Bind(&HeadlessShell::OnScreenshotCaptured, |
| 295 weak_factory_.GetWeakPtr())); | 312 weak_factory_.GetWeakPtr())); |
| 296 } | 313 } |
| 297 | 314 |
| 298 void OnExpressionResult(std::unique_ptr<runtime::EvaluateResult> result) { | 315 void HeadlessShell::OnScreenshotCaptured( |
| 299 std::unique_ptr<base::Value> value = result->Serialize(); | 316 std::unique_ptr<page::CaptureScreenshotResult> result) { |
| 300 std::string result_json; | 317 base::FilePath file_name = |
| 301 base::JSONWriter::Write(*value, &result_json); | 318 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( |
| 302 printf("%s\n", result_json.c_str()); | 319 switches::kScreenshot); |
| 303 InputExpression(); | 320 if (file_name.empty()) { |
| 304 } | 321 file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); |
| 305 | 322 } |
| 306 void CaptureScreenshot() { | 323 |
| 307 devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( | 324 screenshot_file_stream_.reset( |
| 308 page::CaptureScreenshotParams::Builder().Build(), | 325 new net::FileStream(browser_->BrowserFileThread())); |
| 309 base::Bind(&HeadlessShell::OnScreenshotCaptured, | 326 const int open_result = screenshot_file_stream_->Open( |
| 310 weak_factory_.GetWeakPtr())); | 327 file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | |
| 311 } | 328 base::File::FLAG_ASYNC, |
| 312 | 329 base::Bind(&HeadlessShell::OnScreenshotFileOpened, |
| 313 void OnScreenshotCaptured( | 330 weak_factory_.GetWeakPtr(), base::Passed(std::move(result)), |
| 314 std::unique_ptr<page::CaptureScreenshotResult> result) { | 331 file_name)); |
| 315 base::FilePath file_name = | 332 if (open_result != net::ERR_IO_PENDING) { |
| 316 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( | 333 // Operation could not be started. |
| 317 switches::kScreenshot); | 334 OnScreenshotFileOpened(nullptr, file_name, open_result); |
| 318 if (file_name.empty()) { | 335 } |
| 319 file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); | 336 } |
| 320 } | 337 |
| 321 | 338 void HeadlessShell::OnScreenshotFileOpened( |
| 322 screenshot_file_stream_.reset( | 339 std::unique_ptr<page::CaptureScreenshotResult> result, |
| 323 new net::FileStream(browser_->BrowserFileThread())); | 340 const base::FilePath file_name, |
| 324 const int open_result = screenshot_file_stream_->Open( | 341 const int open_result) { |
| 325 file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | | 342 if (open_result != net::OK) { |
| 326 base::File::FLAG_ASYNC, | 343 LOG(ERROR) << "Writing screenshot to file " << file_name.value() |
| 327 base::Bind(&HeadlessShell::OnScreenshotFileOpened, | 344 << " was unsuccessful, could not open file: " |
| 328 weak_factory_.GetWeakPtr(), base::Passed(std::move(result)), | 345 << net::ErrorToString(open_result); |
| 329 file_name)); | 346 return; |
| 330 if (open_result != net::ERR_IO_PENDING) { | 347 } |
| 331 // Operation could not be started. | 348 |
| 332 OnScreenshotFileOpened(nullptr, file_name, open_result); | 349 std::string decoded_png; |
| 333 } | 350 base::Base64Decode(result->GetData(), &decoded_png); |
| 334 } | 351 scoped_refptr<net::IOBufferWithSize> buf = |
| 335 | 352 new net::IOBufferWithSize(decoded_png.size()); |
| 336 void OnScreenshotFileOpened( | 353 memcpy(buf->data(), decoded_png.data(), decoded_png.size()); |
| 337 std::unique_ptr<page::CaptureScreenshotResult> result, | 354 const int write_result = screenshot_file_stream_->Write( |
| 338 const base::FilePath file_name, | 355 buf.get(), buf->size(), |
| 339 const int open_result) { | 356 base::Bind(&HeadlessShell::OnScreenshotFileWritten, |
| 340 if (open_result != net::OK) { | 357 weak_factory_.GetWeakPtr(), file_name, buf->size())); |
| 341 LOG(ERROR) << "Writing screenshot to file " << file_name.value() | 358 if (write_result != net::ERR_IO_PENDING) { |
| 342 << " was unsuccessful, could not open file: " | 359 // Operation may have completed successfully or failed. |
| 343 << net::ErrorToString(open_result); | 360 OnScreenshotFileWritten(file_name, buf->size(), write_result); |
| 344 return; | 361 } |
| 345 } | 362 } |
| 346 | 363 |
| 347 std::string decoded_png; | 364 void HeadlessShell::OnScreenshotFileWritten(const base::FilePath file_name, |
| 348 base::Base64Decode(result->GetData(), &decoded_png); | 365 const int length, |
| 349 scoped_refptr<net::IOBufferWithSize> buf = | 366 const int write_result) { |
| 350 new net::IOBufferWithSize(decoded_png.size()); | 367 if (write_result < length) { |
| 351 memcpy(buf->data(), decoded_png.data(), decoded_png.size()); | 368 // TODO(eseckler): Support recovering from partial writes. |
| 352 const int write_result = screenshot_file_stream_->Write( | 369 LOG(ERROR) << "Writing screenshot to file " << file_name.value() |
| 353 buf.get(), buf->size(), | 370 << " was unsuccessful: " << net::ErrorToString(write_result); |
| 354 base::Bind(&HeadlessShell::OnScreenshotFileWritten, | 371 } else { |
| 355 weak_factory_.GetWeakPtr(), file_name, buf->size())); | 372 LOG(INFO) << "Screenshot written to file " << file_name.value() << "." |
| 356 if (write_result != net::ERR_IO_PENDING) { | 373 << std::endl; |
| 357 // Operation may have completed successfully or failed. | 374 } |
| 358 OnScreenshotFileWritten(file_name, buf->size(), write_result); | 375 int close_result = screenshot_file_stream_->Close(base::Bind( |
| 359 } | 376 &HeadlessShell::OnScreenshotFileClosed, weak_factory_.GetWeakPtr())); |
| 360 } | 377 if (close_result != net::ERR_IO_PENDING) { |
| 361 | 378 // Operation could not be started. |
| 362 void OnScreenshotFileWritten(const base::FilePath file_name, | 379 OnScreenshotFileClosed(close_result); |
| 363 const int length, | 380 } |
| 364 const int write_result) { | 381 } |
| 365 if (write_result < length) { | 382 |
| 366 // TODO(eseckler): Support recovering from partial writes. | 383 void HeadlessShell::OnScreenshotFileClosed(const int close_result) { |
| 367 LOG(ERROR) << "Writing screenshot to file " << file_name.value() | 384 Shutdown(); |
| 368 << " was unsuccessful: " << net::ErrorToString(write_result); | 385 } |
| 369 } else { | 386 |
| 370 LOG(INFO) << "Screenshot written to file " << file_name.value() << "." | 387 bool HeadlessShell::RemoteDebuggingEnabled() const { |
| 371 << std::endl; | 388 const base::CommandLine& command_line = |
| 372 } | 389 *base::CommandLine::ForCurrentProcess(); |
| 373 int close_result = screenshot_file_stream_->Close(base::Bind( | 390 return command_line.HasSwitch(switches::kRemoteDebuggingPort); |
| 374 &HeadlessShell::OnScreenshotFileClosed, weak_factory_.GetWeakPtr())); | 391 } |
| 375 if (close_result != net::ERR_IO_PENDING) { | |
| 376 // Operation could not be started. | |
| 377 OnScreenshotFileClosed(close_result); | |
| 378 } | |
| 379 } | |
| 380 | |
| 381 void OnScreenshotFileClosed(const int close_result) { Shutdown(); } | |
| 382 | |
| 383 bool RemoteDebuggingEnabled() const { | |
| 384 const base::CommandLine& command_line = | |
| 385 *base::CommandLine::ForCurrentProcess(); | |
| 386 return command_line.HasSwitch(switches::kRemoteDebuggingPort); | |
| 387 } | |
| 388 | |
| 389 private: | |
| 390 GURL url_; | |
| 391 HeadlessBrowser* browser_; // Not owned. | |
| 392 std::unique_ptr<HeadlessDevToolsClient> devtools_client_; | |
| 393 HeadlessWebContents* web_contents_; | |
| 394 bool processed_page_ready_; | |
| 395 std::unique_ptr<net::FileStream> screenshot_file_stream_; | |
| 396 HeadlessBrowserContext* browser_context_; | |
| 397 std::unique_ptr<DeterministicDispatcher> deterministic_dispatcher_; | |
| 398 base::WeakPtrFactory<HeadlessShell> weak_factory_; | |
| 399 | |
| 400 DISALLOW_COPY_AND_ASSIGN(HeadlessShell); | |
| 401 }; | |
| 402 | 392 |
| 403 bool ValidateCommandLine(const base::CommandLine& command_line) { | 393 bool ValidateCommandLine(const base::CommandLine& command_line) { |
| 404 if (!command_line.HasSwitch(switches::kRemoteDebuggingPort)) { | 394 if (!command_line.HasSwitch(switches::kRemoteDebuggingPort)) { |
| 405 if (command_line.GetArgs().size() <= 1) | 395 if (command_line.GetArgs().size() <= 1) |
| 406 return true; | 396 return true; |
| 407 LOG(ERROR) << "Open multiple tabs is only supported when the " | 397 LOG(ERROR) << "Open multiple tabs is only supported when the " |
| 408 << "remote debug port is set."; | 398 << "remote debug port is set."; |
| 409 return false; | 399 return false; |
| 410 } | 400 } |
| 411 if (command_line.HasSwitch(switches::kDumpDom)) { | 401 if (command_line.HasSwitch(switches::kDumpDom)) { |
| (...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 516 builder.SetOverrideWebPreferencesCallback(base::Bind([]( | 506 builder.SetOverrideWebPreferencesCallback(base::Bind([]( |
| 517 WebPreferences* preferences) { preferences->hide_scrollbars = true; })); | 507 WebPreferences* preferences) { preferences->hide_scrollbars = true; })); |
| 518 } | 508 } |
| 519 | 509 |
| 520 return HeadlessBrowserMain( | 510 return HeadlessBrowserMain( |
| 521 builder.Build(), | 511 builder.Build(), |
| 522 base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); | 512 base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); |
| 523 } | 513 } |
| 524 | 514 |
| 525 } // namespace headless | 515 } // namespace headless |
| OLD | NEW |