| // Copyright (c) 2012 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. |
| |
| // Implements a Browser Helper Object (BHO) which opens a socket |
| // and waits to receive URLs over it. Visits those URLs, measuring |
| // how long it takes between the start of navigation and the |
| // DocumentComplete event, and returns the time in milliseconds as |
| // a string to the caller. |
| |
| #include "stdafx.h" |
| #include "MeasurePageLoadTimeBHO.h" |
| |
| #define MAX_URL 1024 // size of URL buffer |
| #define MAX_PAGELOADTIME (4*60*1000) // assume all pages take < 4 minutes |
| #define PORT 42492 // port to listen on. Also jhaas's |
| // old MSFT employee number |
| |
| |
| // Static function to serve as thread entry point, takes a "this" |
| // pointer as pParam and calls the method in the object |
| static DWORD WINAPI ProcessPageTimeRequests(LPVOID pThis) { |
| reinterpret_cast<CMeasurePageLoadTimeBHO*>(pThis)->ProcessPageTimeRequests(); |
| |
| return 0; |
| } |
| |
| |
| STDMETHODIMP CMeasurePageLoadTimeBHO::SetSite(IUnknown* pUnkSite) |
| { |
| if (pUnkSite != NULL) |
| { |
| // Cache the pointer to IWebBrowser2. |
| HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser); |
| if (SUCCEEDED(hr)) |
| { |
| // Register to sink events from DWebBrowserEvents2. |
| hr = DispEventAdvise(m_spWebBrowser); |
| if (SUCCEEDED(hr)) |
| { |
| m_fAdvised = TRUE; |
| } |
| |
| // Stash the interface in the global interface table |
| CComGITPtr<IWebBrowser2> git(m_spWebBrowser); |
| m_dwCookie = git.Detach(); |
| |
| // Create the event to be signaled when navigation completes. |
| // Start it in nonsignaled state, and allow it to be triggered |
| // when the initial page load is done. |
| m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); |
| |
| // Create a thread to wait on the socket |
| HANDLE hThread = CreateThread(NULL, 0, ::ProcessPageTimeRequests, this, 0, NULL); |
| } |
| } |
| else |
| { |
| // Unregister event sink. |
| if (m_fAdvised) |
| { |
| DispEventUnadvise(m_spWebBrowser); |
| m_fAdvised = FALSE; |
| } |
| |
| // Release cached pointers and other resources here. |
| m_spWebBrowser.Release(); |
| } |
| |
| // Call base class implementation. |
| return IObjectWithSiteImpl<CMeasurePageLoadTimeBHO>::SetSite(pUnkSite); |
| } |
| |
| |
| void STDMETHODCALLTYPE CMeasurePageLoadTimeBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL) |
| { |
| if (pDisp == m_spWebBrowser) |
| { |
| // Fire the event when the page is done loading |
| // to unblock the other thread. |
| SetEvent(m_hEvent); |
| } |
| } |
| |
| |
| void CMeasurePageLoadTimeBHO::ProcessPageTimeRequests() |
| { |
| CoInitialize(NULL); |
| |
| // The event will start in nonsignaled state, meaning that |
| // the initial page load isn't done yet. Wait for that to |
| // finish before doing anything. |
| // |
| // It seems to be the case that the BHO will get loaded |
| // and SetSite called always before the initial page load |
| // even begins, but just to be on the safe side, we won't |
| // wait indefinitely. |
| WaitForSingleObject(m_hEvent, MAX_PAGELOADTIME); |
| |
| // Retrieve the web browser interface from the global table |
| CComGITPtr<IWebBrowser2> git(m_dwCookie); |
| IWebBrowser2* browser; |
| git.CopyTo(&browser); |
| |
| // Create a listening socket |
| m_sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); |
| if (m_sockListen == SOCKET_ERROR) |
| ErrorExit(); |
| |
| BOOL on = TRUE; |
| if (setsockopt(m_sockListen, SOL_SOCKET, SO_REUSEADDR, |
| (const char*)&on, sizeof(on))) |
| ErrorExit(); |
| |
| // Bind the listening socket |
| SOCKADDR_IN addrBind; |
| |
| addrBind.sin_family = AF_INET; |
| addrBind.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| addrBind.sin_port = htons(PORT); |
| |
| if (bind(m_sockListen, (sockaddr*)&addrBind, sizeof(addrBind))) |
| ErrorExit(); |
| |
| // Listen for incoming connections |
| if (listen(m_sockListen, 1)) |
| ErrorExit(); |
| |
| // Ensure the socket is blocking... it should be by default, but |
| // it can't hurt to make sure |
| unsigned long nNonblocking = 0; |
| if (ioctlsocket(m_sockListen, FIONBIO, &nNonblocking)) |
| ErrorExit(); |
| |
| m_sockTransport = 0; |
| |
| // Loop indefinitely waiting for connections |
| while(1) |
| { |
| SOCKADDR_IN addrConnected; |
| int sConnected = sizeof(addrConnected); |
| |
| // Wait for a client to connect and send a URL |
| m_sockTransport = accept( |
| m_sockListen, (sockaddr*)&addrConnected, &sConnected); |
| |
| if (m_sockTransport == SOCKET_ERROR) |
| ErrorExit(); |
| |
| char pbBuffer[MAX_URL], strURL[MAX_URL]; |
| DWORD cbRead, cbWritten; |
| |
| bool fDone = false; |
| |
| // Loop until we're done with this client |
| while (!fDone) |
| { |
| *strURL = '\0'; |
| bool fReceivedCR = false; |
| |
| do |
| { |
| // Only receive up to the first carriage return |
| cbRead = recv(m_sockTransport, pbBuffer, MAX_URL-1, MSG_PEEK); |
| |
| // An error on read most likely means that the remote peer |
| // closed the connection. Go back to waiting |
| if (cbRead == 0) |
| { |
| fDone = true; |
| break; |
| } |
| |
| // Null terminate the received characters so strchr() is safe |
| pbBuffer[cbRead] = '\0'; |
| |
| if(char* pchFirstCR = strchr(pbBuffer, '\n')) |
| { |
| cbRead = (DWORD)(pchFirstCR - pbBuffer + 1); |
| fReceivedCR = true; |
| } |
| |
| // The below call will not block, since we determined with |
| // MSG_PEEK that at least cbRead bytes are in the TCP receive buffer |
| recv(m_sockTransport, pbBuffer, cbRead, 0); |
| pbBuffer[cbRead] = '\0'; |
| |
| strcat_s(strURL, sizeof(strURL), pbBuffer); |
| } while (!fReceivedCR); |
| |
| // If an error occurred while reading, exit this loop |
| if (fDone) |
| break; |
| |
| // Strip the trailing CR and/or LF |
| int i; |
| for (i = (int)strlen(strURL)-1; i >= 0 && isspace(strURL[i]); i--) |
| { |
| strURL[i] = '\0'; |
| } |
| |
| if (i < 0) |
| { |
| // Sending a carriage return on a line by itself means that |
| // the client is done making requests |
| fDone = true; |
| } |
| else |
| { |
| // Send the browser to the requested URL |
| CComVariant vNavFlags( navNoReadFromCache ); |
| CComVariant vTargetFrame("_self"); |
| CComVariant vPostData(""); |
| CComVariant vHTTPHeaders(""); |
| |
| ResetEvent(m_hEvent); |
| DWORD dwStartTime = GetTickCount(); |
| |
| HRESULT hr = browser->Navigate( |
| CComBSTR(strURL), |
| &vNavFlags, |
| &vTargetFrame, // TargetFrameName |
| &vPostData, // PostData |
| &vHTTPHeaders // Headers |
| ); |
| |
| // The main browser thread will call OnDocumentComplete() when |
| // the page is done loading, which will in turn trigger |
| // m_hEvent. Wait here until then; the event will reset itself |
| // once this thread is released |
| if (WaitForSingleObject(m_hEvent, MAX_PAGELOADTIME) == WAIT_TIMEOUT) |
| { |
| sprintf_s(pbBuffer, sizeof(pbBuffer), "%s,timeout\n", strURL); |
| |
| browser->Stop(); |
| } |
| else |
| { |
| // Format the elapsed time as a string |
| DWORD dwLoadTime = GetTickCount() - dwStartTime; |
| sprintf_s( |
| pbBuffer, sizeof(pbBuffer), "%s,%d\n", strURL, dwLoadTime); |
| } |
| |
| // Send the result. Just in case the TCP buffer can't handle |
| // the whole thing, send in parts if necessary |
| char *chSend = pbBuffer; |
| |
| while (*chSend) |
| { |
| cbWritten = send( |
| m_sockTransport, chSend, (int)strlen(chSend), 0); |
| |
| // Error on send probably means connection reset by peer |
| if (cbWritten == 0) |
| { |
| fDone = true; |
| break; |
| } |
| |
| chSend += cbWritten; |
| } |
| } |
| } |
| |
| // Close the transport socket and wait for another connection |
| closesocket(m_sockTransport); |
| m_sockTransport = 0; |
| } |
| } |
| |
| |
| void CMeasurePageLoadTimeBHO::ErrorExit() |
| { |
| // Unlink from IE, close the sockets, then terminate this |
| // thread |
| SetSite(NULL); |
| |
| if (m_sockTransport && m_sockTransport != SOCKET_ERROR) |
| { |
| closesocket(m_sockTransport); |
| m_sockTransport = 0; |
| } |
| |
| if (m_sockListen && m_sockListen != SOCKET_ERROR) |
| { |
| closesocket(m_sockListen); |
| m_sockListen = 0; |
| } |
| |
| TerminateThread(GetCurrentThread(), -1); |
| } |