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 <iostream> | 5 #include <iostream> |
6 #include <memory> | 6 #include <memory> |
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" |
(...skipping 15 matching lines...) Expand all Loading... |
26 #include "headless/public/headless_devtools_target.h" | 26 #include "headless/public/headless_devtools_target.h" |
27 #include "headless/public/headless_web_contents.h" | 27 #include "headless/public/headless_web_contents.h" |
28 #include "headless/public/util/deterministic_dispatcher.h" | 28 #include "headless/public/util/deterministic_dispatcher.h" |
29 #include "headless/public/util/deterministic_http_protocol_handler.h" | 29 #include "headless/public/util/deterministic_http_protocol_handler.h" |
30 #include "net/base/file_stream.h" | 30 #include "net/base/file_stream.h" |
31 #include "net/base/io_buffer.h" | 31 #include "net/base/io_buffer.h" |
32 #include "net/base/ip_address.h" | 32 #include "net/base/ip_address.h" |
33 #include "net/base/net_errors.h" | 33 #include "net/base/net_errors.h" |
34 #include "ui/gfx/geometry/size.h" | 34 #include "ui/gfx/geometry/size.h" |
35 | 35 |
36 using headless::HeadlessBrowser; | 36 namespace headless { |
37 using headless::HeadlessBrowserContext; | |
38 using headless::HeadlessDevToolsClient; | |
39 using headless::HeadlessWebContents; | |
40 namespace emulation = headless::emulation; | |
41 namespace page = headless::page; | |
42 namespace runtime = headless::runtime; | |
43 | |
44 namespace { | 37 namespace { |
45 // Address where to listen to incoming DevTools connections. | 38 // Address where to listen to incoming DevTools connections. |
46 const char kDevToolsHttpServerAddress[] = "127.0.0.1"; | 39 const char kDevToolsHttpServerAddress[] = "127.0.0.1"; |
47 // Default file name for screenshot. Can be overriden by "--screenshot" switch. | 40 // Default file name for screenshot. Can be overriden by "--screenshot" switch. |
48 const char kDefaultScreenshotFileName[] = "screenshot.png"; | 41 const char kDefaultScreenshotFileName[] = "screenshot.png"; |
49 | 42 |
50 bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { | 43 bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { |
51 int width, height = 0; | 44 int width, height = 0; |
52 if (sscanf(window_size.c_str(), "%dx%d", &width, &height) >= 2 && | 45 if (sscanf(window_size.c_str(), "%dx%d", &width, &height) >= 2 && |
53 width >= 0 && height >= 0) { | 46 width >= 0 && height >= 0) { |
54 parsed_window_size->set_width(width); | 47 parsed_window_size->set_width(width); |
55 parsed_window_size->set_height(height); | 48 parsed_window_size->set_height(height); |
56 return true; | 49 return true; |
57 } | 50 } |
58 return false; | 51 return false; |
59 } | 52 } |
60 } // namespace | 53 } // namespace |
61 | 54 |
62 // A sample application which demonstrates the use of the headless API. | 55 // An application which implements a simple headless browser. |
63 class HeadlessShell : public HeadlessWebContents::Observer, | 56 class HeadlessShell : public HeadlessWebContents::Observer, |
64 emulation::ExperimentalObserver, | 57 emulation::ExperimentalObserver, |
65 page::Observer { | 58 page::Observer { |
66 public: | 59 public: |
67 HeadlessShell() | 60 HeadlessShell() |
68 : browser_(nullptr), | 61 : browser_(nullptr), |
69 devtools_client_(HeadlessDevToolsClient::Create()), | 62 devtools_client_(HeadlessDevToolsClient::Create()), |
70 web_contents_(nullptr), | 63 web_contents_(nullptr), |
71 processed_page_ready_(false), | 64 processed_page_ready_(false), |
72 browser_context_(nullptr) {} | 65 browser_context_(nullptr) {} |
73 ~HeadlessShell() override {} | 66 ~HeadlessShell() override {} |
74 | 67 |
75 void OnStart(HeadlessBrowser* browser) { | 68 void OnStart(HeadlessBrowser* browser) { |
76 browser_ = browser; | 69 browser_ = browser; |
77 | 70 |
78 HeadlessBrowserContext::Builder context_builder = | 71 HeadlessBrowserContext::Builder context_builder = |
79 browser_->CreateBrowserContextBuilder(); | 72 browser_->CreateBrowserContextBuilder(); |
80 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 73 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
81 headless::switches::kDeterministicFetch)) { | 74 switches::kDeterministicFetch)) { |
82 deterministic_dispatcher_.reset( | 75 deterministic_dispatcher_.reset( |
83 new headless::DeterministicDispatcher(browser_->BrowserIOThread())); | 76 new DeterministicDispatcher(browser_->BrowserIOThread())); |
84 | 77 |
85 headless::ProtocolHandlerMap protocol_handlers; | 78 ProtocolHandlerMap protocol_handlers; |
86 protocol_handlers[url::kHttpScheme] = | 79 protocol_handlers[url::kHttpScheme] = |
87 base::MakeUnique<headless::DeterministicHttpProtocolHandler>( | 80 base::MakeUnique<DeterministicHttpProtocolHandler>( |
88 deterministic_dispatcher_.get(), browser->BrowserIOThread()); | 81 deterministic_dispatcher_.get(), browser->BrowserIOThread()); |
89 protocol_handlers[url::kHttpsScheme] = | 82 protocol_handlers[url::kHttpsScheme] = |
90 base::MakeUnique<headless::DeterministicHttpProtocolHandler>( | 83 base::MakeUnique<DeterministicHttpProtocolHandler>( |
91 deterministic_dispatcher_.get(), browser->BrowserIOThread()); | 84 deterministic_dispatcher_.get(), browser->BrowserIOThread()); |
92 | 85 |
93 context_builder.SetProtocolHandlers(std::move(protocol_handlers)); | 86 context_builder.SetProtocolHandlers(std::move(protocol_handlers)); |
94 } | 87 } |
95 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 88 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
96 headless::switches::kHideScrollbars)) { | 89 switches::kHideScrollbars)) { |
97 context_builder.SetOverrideWebPreferencesCallback( | 90 context_builder.SetOverrideWebPreferencesCallback( |
98 base::Bind([](headless::WebPreferences* preferences) { | 91 base::Bind([](WebPreferences* preferences) { |
99 preferences->hide_scrollbars = true; | 92 preferences->hide_scrollbars = true; |
100 })); | 93 })); |
101 } | 94 } |
102 browser_context_ = context_builder.Build(); | 95 browser_context_ = context_builder.Build(); |
103 | 96 |
104 HeadlessWebContents::Builder builder( | 97 HeadlessWebContents::Builder builder( |
105 browser_context_->CreateWebContentsBuilder()); | 98 browser_context_->CreateWebContentsBuilder()); |
106 base::CommandLine::StringVector args = | 99 base::CommandLine::StringVector args = |
107 base::CommandLine::ForCurrentProcess()->GetArgs(); | 100 base::CommandLine::ForCurrentProcess()->GetArgs(); |
108 | 101 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
140 return; | 133 return; |
141 web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); | 134 web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); |
142 devtools_client_->GetPage()->AddObserver(this); | 135 devtools_client_->GetPage()->AddObserver(this); |
143 devtools_client_->GetPage()->Enable(); | 136 devtools_client_->GetPage()->Enable(); |
144 // Check if the document had already finished loading by the time we | 137 // Check if the document had already finished loading by the time we |
145 // attached. | 138 // attached. |
146 | 139 |
147 devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); | 140 devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); |
148 | 141 |
149 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 142 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
150 headless::switches::kVirtualTimeBudget)) { | 143 switches::kVirtualTimeBudget)) { |
151 std::string budget_ms_ascii = | 144 std::string budget_ms_ascii = |
152 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | 145 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
153 headless::switches::kVirtualTimeBudget); | 146 switches::kVirtualTimeBudget); |
154 int budget_ms; | 147 int budget_ms; |
155 CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) | 148 CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) |
156 << "Expected an integer value for --virtual-time-budget="; | 149 << "Expected an integer value for --virtual-time-budget="; |
157 devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( | 150 devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( |
158 emulation::SetVirtualTimePolicyParams::Builder() | 151 emulation::SetVirtualTimePolicyParams::Builder() |
159 .SetPolicy(emulation::VirtualTimePolicy:: | 152 .SetPolicy(emulation::VirtualTimePolicy:: |
160 PAUSE_IF_NETWORK_FETCHES_PENDING) | 153 PAUSE_IF_NETWORK_FETCHES_PENDING) |
161 .SetBudget(budget_ms) | 154 .SetBudget(budget_ms) |
162 .Build()); | 155 .Build()); |
163 } else { | 156 } else { |
(...skipping 29 matching lines...) Expand all Loading... |
193 | 186 |
194 // emulation::Observer implementation: | 187 // emulation::Observer implementation: |
195 void OnVirtualTimeBudgetExpired( | 188 void OnVirtualTimeBudgetExpired( |
196 const emulation::VirtualTimeBudgetExpiredParams& params) override { | 189 const emulation::VirtualTimeBudgetExpiredParams& params) override { |
197 OnPageReady(); | 190 OnPageReady(); |
198 } | 191 } |
199 | 192 |
200 // page::Observer implementation: | 193 // page::Observer implementation: |
201 void OnLoadEventFired(const page::LoadEventFiredParams& params) override { | 194 void OnLoadEventFired(const page::LoadEventFiredParams& params) override { |
202 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 195 if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
203 headless::switches::kVirtualTimeBudget)) { | 196 switches::kVirtualTimeBudget)) { |
204 return; | 197 return; |
205 } | 198 } |
206 OnPageReady(); | 199 OnPageReady(); |
207 } | 200 } |
208 | 201 |
209 void OnPageReady() { | 202 void OnPageReady() { |
210 if (processed_page_ready_) | 203 if (processed_page_ready_) |
211 return; | 204 return; |
212 processed_page_ready_ = true; | 205 processed_page_ready_ = true; |
213 | 206 |
214 if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 207 if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDumpDom)) { |
215 headless::switches::kDumpDom)) { | |
216 FetchDom(); | 208 FetchDom(); |
217 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 209 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
218 headless::switches::kRepl)) { | 210 switches::kRepl)) { |
219 std::cout | 211 std::cout |
220 << "Type a Javascript expression to evaluate or \"quit\" to exit." | 212 << "Type a Javascript expression to evaluate or \"quit\" to exit." |
221 << std::endl; | 213 << std::endl; |
222 InputExpression(); | 214 InputExpression(); |
223 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( | 215 } else if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
224 headless::switches::kScreenshot)) { | 216 switches::kScreenshot)) { |
225 CaptureScreenshot(); | 217 CaptureScreenshot(); |
226 } else { | 218 } else { |
227 Shutdown(); | 219 Shutdown(); |
228 } | 220 } |
229 } | 221 } |
230 | 222 |
231 void FetchDom() { | 223 void FetchDom() { |
232 devtools_client_->GetRuntime()->Evaluate( | 224 devtools_client_->GetRuntime()->Evaluate( |
233 "document.body.innerHTML", | 225 "document.body.innerHTML", |
234 base::Bind(&HeadlessShell::OnDomFetched, base::Unretained(this))); | 226 base::Bind(&HeadlessShell::OnDomFetched, base::Unretained(this))); |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
274 devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( | 266 devtools_client_->GetPage()->GetExperimental()->CaptureScreenshot( |
275 page::CaptureScreenshotParams::Builder().Build(), | 267 page::CaptureScreenshotParams::Builder().Build(), |
276 base::Bind(&HeadlessShell::OnScreenshotCaptured, | 268 base::Bind(&HeadlessShell::OnScreenshotCaptured, |
277 base::Unretained(this))); | 269 base::Unretained(this))); |
278 } | 270 } |
279 | 271 |
280 void OnScreenshotCaptured( | 272 void OnScreenshotCaptured( |
281 std::unique_ptr<page::CaptureScreenshotResult> result) { | 273 std::unique_ptr<page::CaptureScreenshotResult> result) { |
282 base::FilePath file_name = | 274 base::FilePath file_name = |
283 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( | 275 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( |
284 headless::switches::kScreenshot); | 276 switches::kScreenshot); |
285 if (file_name.empty()) { | 277 if (file_name.empty()) { |
286 file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); | 278 file_name = base::FilePath().AppendASCII(kDefaultScreenshotFileName); |
287 } | 279 } |
288 | 280 |
289 screenshot_file_stream_.reset( | 281 screenshot_file_stream_.reset( |
290 new net::FileStream(browser_->BrowserFileThread())); | 282 new net::FileStream(browser_->BrowserFileThread())); |
291 const int open_result = screenshot_file_stream_->Open( | 283 const int open_result = screenshot_file_stream_->Open( |
292 file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | | 284 file_name, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE | |
293 base::File::FLAG_ASYNC, | 285 base::File::FLAG_ASYNC, |
294 base::Bind(&HeadlessShell::OnScreenshotFileOpened, | 286 base::Bind(&HeadlessShell::OnScreenshotFileOpened, |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
343 // Operation could not be started. | 335 // Operation could not be started. |
344 OnScreenshotFileClosed(close_result); | 336 OnScreenshotFileClosed(close_result); |
345 } | 337 } |
346 } | 338 } |
347 | 339 |
348 void OnScreenshotFileClosed(const int close_result) { Shutdown(); } | 340 void OnScreenshotFileClosed(const int close_result) { Shutdown(); } |
349 | 341 |
350 bool RemoteDebuggingEnabled() const { | 342 bool RemoteDebuggingEnabled() const { |
351 const base::CommandLine& command_line = | 343 const base::CommandLine& command_line = |
352 *base::CommandLine::ForCurrentProcess(); | 344 *base::CommandLine::ForCurrentProcess(); |
353 return command_line.HasSwitch(switches::kRemoteDebuggingPort); | 345 return command_line.HasSwitch(::switches::kRemoteDebuggingPort); |
354 } | 346 } |
355 | 347 |
356 private: | 348 private: |
357 GURL url_; | 349 GURL url_; |
358 HeadlessBrowser* browser_; // Not owned. | 350 HeadlessBrowser* browser_; // Not owned. |
359 std::unique_ptr<HeadlessDevToolsClient> devtools_client_; | 351 std::unique_ptr<HeadlessDevToolsClient> devtools_client_; |
360 HeadlessWebContents* web_contents_; | 352 HeadlessWebContents* web_contents_; |
361 bool processed_page_ready_; | 353 bool processed_page_ready_; |
362 std::unique_ptr<net::FileStream> screenshot_file_stream_; | 354 std::unique_ptr<net::FileStream> screenshot_file_stream_; |
363 HeadlessBrowserContext* browser_context_; | 355 HeadlessBrowserContext* browser_context_; |
364 std::unique_ptr<headless::DeterministicDispatcher> deterministic_dispatcher_; | 356 std::unique_ptr<DeterministicDispatcher> deterministic_dispatcher_; |
365 | 357 |
366 DISALLOW_COPY_AND_ASSIGN(HeadlessShell); | 358 DISALLOW_COPY_AND_ASSIGN(HeadlessShell); |
367 }; | 359 }; |
368 | 360 |
369 int main(int argc, const char** argv) { | 361 int HeadlessShellMain(int argc, const char** argv) { |
370 headless::RunChildProcessIfNeeded(argc, argv); | 362 RunChildProcessIfNeeded(argc, argv); |
371 HeadlessShell shell; | 363 HeadlessShell shell; |
372 HeadlessBrowser::Options::Builder builder(argc, argv); | 364 HeadlessBrowser::Options::Builder builder(argc, argv); |
373 | 365 |
374 // Enable devtools if requested. | 366 // Enable devtools if requested. |
375 base::CommandLine command_line(argc, argv); | 367 base::CommandLine command_line(argc, argv); |
376 if (command_line.HasSwitch(switches::kRemoteDebuggingPort)) { | 368 if (command_line.HasSwitch(::switches::kRemoteDebuggingPort)) { |
377 std::string address = kDevToolsHttpServerAddress; | 369 std::string address = kDevToolsHttpServerAddress; |
378 if (command_line.HasSwitch(headless::switches::kRemoteDebuggingAddress)) { | 370 if (command_line.HasSwitch(switches::kRemoteDebuggingAddress)) { |
379 address = command_line.GetSwitchValueASCII( | 371 address = |
380 headless::switches::kRemoteDebuggingAddress); | 372 command_line.GetSwitchValueASCII(switches::kRemoteDebuggingAddress); |
381 net::IPAddress parsed_address; | 373 net::IPAddress parsed_address; |
382 if (!net::ParseURLHostnameToAddress(address, &parsed_address)) { | 374 if (!net::ParseURLHostnameToAddress(address, &parsed_address)) { |
383 LOG(ERROR) << "Invalid devtools server address"; | 375 LOG(ERROR) << "Invalid devtools server address"; |
384 return EXIT_FAILURE; | 376 return EXIT_FAILURE; |
385 } | 377 } |
386 } | 378 } |
387 int parsed_port; | 379 int parsed_port; |
388 std::string port_str = | 380 std::string port_str = |
389 command_line.GetSwitchValueASCII(switches::kRemoteDebuggingPort); | 381 command_line.GetSwitchValueASCII(::switches::kRemoteDebuggingPort); |
390 if (!base::StringToInt(port_str, &parsed_port) || | 382 if (!base::StringToInt(port_str, &parsed_port) || |
391 !base::IsValueInRangeForNumericType<uint16_t>(parsed_port)) { | 383 !base::IsValueInRangeForNumericType<uint16_t>(parsed_port)) { |
392 LOG(ERROR) << "Invalid devtools server port"; | 384 LOG(ERROR) << "Invalid devtools server port"; |
393 return EXIT_FAILURE; | 385 return EXIT_FAILURE; |
394 } | 386 } |
395 net::IPAddress devtools_address; | 387 net::IPAddress devtools_address; |
396 bool result = devtools_address.AssignFromIPLiteral(address); | 388 bool result = devtools_address.AssignFromIPLiteral(address); |
397 DCHECK(result); | 389 DCHECK(result); |
398 builder.EnableDevToolsServer(net::IPEndPoint( | 390 builder.EnableDevToolsServer(net::IPEndPoint( |
399 devtools_address, base::checked_cast<uint16_t>(parsed_port))); | 391 devtools_address, base::checked_cast<uint16_t>(parsed_port))); |
400 } | 392 } |
401 | 393 |
402 if (command_line.HasSwitch(headless::switches::kProxyServer)) { | 394 if (command_line.HasSwitch(switches::kProxyServer)) { |
403 std::string proxy_server = | 395 std::string proxy_server = |
404 command_line.GetSwitchValueASCII(headless::switches::kProxyServer); | 396 command_line.GetSwitchValueASCII(switches::kProxyServer); |
405 net::HostPortPair parsed_proxy_server = | 397 net::HostPortPair parsed_proxy_server = |
406 net::HostPortPair::FromString(proxy_server); | 398 net::HostPortPair::FromString(proxy_server); |
407 if (parsed_proxy_server.host().empty() || !parsed_proxy_server.port()) { | 399 if (parsed_proxy_server.host().empty() || !parsed_proxy_server.port()) { |
408 LOG(ERROR) << "Malformed proxy server url"; | 400 LOG(ERROR) << "Malformed proxy server url"; |
409 return EXIT_FAILURE; | 401 return EXIT_FAILURE; |
410 } | 402 } |
411 builder.SetProxyServer(parsed_proxy_server); | 403 builder.SetProxyServer(parsed_proxy_server); |
412 } | 404 } |
413 | 405 |
414 if (command_line.HasSwitch(switches::kHostResolverRules)) { | 406 if (command_line.HasSwitch(::switches::kHostResolverRules)) { |
415 builder.SetHostResolverRules( | 407 builder.SetHostResolverRules( |
416 command_line.GetSwitchValueASCII(switches::kHostResolverRules)); | 408 command_line.GetSwitchValueASCII(::switches::kHostResolverRules)); |
417 } | 409 } |
418 | 410 |
419 if (command_line.HasSwitch(headless::switches::kUseGL)) { | 411 if (command_line.HasSwitch(switches::kUseGL)) { |
420 builder.SetGLImplementation( | 412 builder.SetGLImplementation( |
421 command_line.GetSwitchValueASCII(headless::switches::kUseGL)); | 413 command_line.GetSwitchValueASCII(switches::kUseGL)); |
422 } | 414 } |
423 | 415 |
424 if (command_line.HasSwitch(headless::switches::kUserDataDir)) { | 416 if (command_line.HasSwitch(switches::kUserDataDir)) { |
425 builder.SetUserDataDir( | 417 builder.SetUserDataDir( |
426 command_line.GetSwitchValuePath(headless::switches::kUserDataDir)); | 418 command_line.GetSwitchValuePath(switches::kUserDataDir)); |
427 builder.SetIncognitoMode(false); | 419 builder.SetIncognitoMode(false); |
428 } | 420 } |
429 | 421 |
430 if (command_line.HasSwitch(headless::switches::kWindowSize)) { | 422 if (command_line.HasSwitch(switches::kWindowSize)) { |
431 std::string window_size = | 423 std::string window_size = |
432 command_line.GetSwitchValueASCII(headless::switches::kWindowSize); | 424 command_line.GetSwitchValueASCII(switches::kWindowSize); |
433 gfx::Size parsed_window_size; | 425 gfx::Size parsed_window_size; |
434 if (!ParseWindowSize(window_size, &parsed_window_size)) { | 426 if (!ParseWindowSize(window_size, &parsed_window_size)) { |
435 LOG(ERROR) << "Malformed window size"; | 427 LOG(ERROR) << "Malformed window size"; |
436 return EXIT_FAILURE; | 428 return EXIT_FAILURE; |
437 } | 429 } |
438 builder.SetWindowSize(parsed_window_size); | 430 builder.SetWindowSize(parsed_window_size); |
439 } | 431 } |
440 | 432 |
441 return HeadlessBrowserMain( | 433 return HeadlessBrowserMain( |
442 builder.Build(), | 434 builder.Build(), |
443 base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); | 435 base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); |
444 } | 436 } |
| 437 |
| 438 } // namespace headless |
OLD | NEW |